Modelo

El objetivo de un modelo es proporcionar un resumen simple de baja dimensión de un conjunto de datos. Idealmente, el modelo capturará patrones generados por el fenómeno de interés e ignorará el “ruido” (es decir, la variación aleatoria que no le interesa).

Tradicionalmente, el enfoque del modelado está en la inferencia o confirmar que una hipótesis es cierta. Hay un par de ideas que nos pueden ayudar a hacer inferencias correctamente:

  1. Cada observación puede usarse para exploración o confirmación, no ambas.

  2. Puede utilizar una observación tantas veces como desee para la exploración, pero solo puede utilizarla una vez para la confirmación. Tan pronto como utilice una observación dos veces, pasará de la confirmación a la exploración.

Esto es necesario porque para confirmar una hipótesis debe utilizar datos independientes de los datos que utilizó para generar la hipótesis. De lo contrario, será demasiado optimista. No hay absolutamente nada de malo en la exploración, pero nunca debe vender un análisis exploratorio como un análisis confirmatorio porque es fundamentalmente engañoso.

Si realmente desea realizar un análisis confirmatorio, un enfoque es dividir sus datos en tres partes antes de comenzar el análisis:

  1. El 60% de sus datos se destina a un conjunto de entrenamiento, training, (o exploración). Puede hacer lo que quiera con estos datos: visualizarlos y ajustarles toneladas de modelos.

  2. El 20% se destina a un conjunto de consultas, query. Puede usar estos datos para comparar modelos o visualizaciones a mano, pero no puede usarlos como parte de un proceso automatizado.

  3. El 20% se retiene para una prueba testing. Solo puede usar estos datos UNA VEZ, para probar su modelo final.

Esta partición le permite explorar los datos de entrenamiento, generando ocasionalmente hipótesis candidatas que verifica con el conjunto de consultas. Cuando esté seguro de que tiene el modelo correcto, puede verificarlo una vez con los datos de prueba.

Tidymodels

Tidymodels, es una interfaz que unifica bajo un único marco cientos de funciones de distintos paquetes, facilitando en gran medida todas las etapas de preprocesado, entrenamiento, optimización y validación de modelos predictivos. Los paquetes principales que forman parte del ecosistema tidymodels son:

  • parsnip: para la definición de modelos. Implementación tidy del caret.

  • recipes: para el preprocesado de datos y feature engineering.

  • rsample: para validar los modelos por métodos de resampling.

  • dials: para crear y manejar el valor de los hiperparámetros.

  • tune: para hacer tuning de modelos.

  • yardstick: para calcular métricas de modelos.

  • workflows: para combinar todos los pasos del preprocesado y modelado en un único objeto.

El procesamiento lo realizan los paquetes rsample y recipes. El modelo principal está en parsnip (el equivalente de Tidy a caret) y para la validación Yardstick. Cabe señalar que caret sigue teniendo la mejor implementación de las tablas de matrices de confusión, con un paquete adaptado ConfusionTableR.

División Train y test

Evaluar la capacidad predictiva de un modelo consiste en comprobar cómo se acercan las predicciones a los valores verdaderos de la variable respuesta. Para poder cuantificarlo de forma correcta, se necesita disponer de un conjunto de observaciones, de las que se conozca la variable respuesta, pero que el modelo no haya “visto”, es decir, que no hayan participado en su ajuste. Con esta finalidad, se divide el conjunto de datos disponibles en un conjunto de entrenamiento (train) y un conjunto de prueba (test).

El tamaño adecuado de las particiones depende en gran medida de la cantidad de datos disponibles y la seguridad que se necesite en la estimación del error, 80%-20% suele dar buenos resultados. El reparto debe hacerse de forma aleatoria o aleatoria-estratificada. Evaluación de modelos (ver link).

Para este ejemplo, analizaremos el conjunto de datos SaratogaHiyses, que se encuentra en el paquete nisaucData, el cual contiene información sobre los precio de 1728 viviendas de Saratoga County, New York, USA en el año 2006. Además del precio, este dataset incluye otras 15 variables:

  • price: precio de la vivienda.

  • lotSize: metros cuadrados de la vivienda.

  • age: antigüedad de la vivienda.

  • landValue: valor del terreno.

  • livingArea: metros cuadrados habitables.

  • pctCollege: porcentaje del vecindario con título universitario.

  • bedrooms: número de dormitorios.

  • firplaces: número de chimeneas.

  • bathrooms: número de cuartos de baño (el valor 0.5 hace referencia a cuartos de baño sin ducha).

  • rooms: número de habitaciones.

  • heating: tipo de calefacción.

  • fuel: tipo de alimentación de la calefacción (gas, electricidad o diesel).

  • sewer: tipo de desagüe.

  • waterfront: si la vivienda tiene vistas al lago.

  • newConstruction: si la vivienda es de nueva construcción.

  • centralAir: si la vivienda tiene aire acondicionado.

El objetivo es obtener un modelo capaz de predecir el precio del alquiler casa. Por lo tanto para poder llamar los datos tomamos el siguiente código:

data("SaratogaHouses", package = "mosaicData")
data <- SaratogaHouses
colnames(data)
 [1] "price"           "lotSize"         "age"             "landValue"       "livingArea"     
 [6] "pctCollege"      "bedrooms"        "fireplaces"      "bathrooms"       "rooms"          
[11] "heating"         "fuel"            "sewer"           "waterfront"      "newConstruction"
[16] "centralAir"     

Con este conjunto de datos procedemos a hacer la división entre train y test.

library(tidymodels)
Registered S3 method overwritten by 'tune':
  method                   from   
  required_pkgs.model_spec parsnip
── Attaching packages ────────────────────────────────────────────────────────────── tidymodels 0.1.3 ──
✓ broom        0.7.8      ✓ recipes      0.1.16
✓ dials        0.0.9      ✓ rsample      0.1.0 
✓ dplyr        1.0.7      ✓ tibble       3.1.2 
✓ ggplot2      3.3.5      ✓ tidyr        1.1.3 
✓ infer        0.5.4      ✓ tune         0.1.5 
✓ modeldata    0.1.0      ✓ workflows    0.2.2 
✓ parsnip      0.1.6      ✓ workflowsets 0.0.2 
✓ purrr        0.3.4      ✓ yardstick    0.0.8 
── Conflicts ───────────────────────────────────────────────────────────────── tidymodels_conflicts() ──
x purrr::discard() masks scales::discard()
x dplyr::filter()  masks stats::filter()
x dplyr::lag()     masks stats::lag()
x recipes::step()  masks stats::step()
• Use tidymodels_prefer() to resolve common conflicts.
# Reparto de datos en train y test
set.seed(123)
split_inicial <- initial_split(
                    data   = data, # datos que queremos dividir
                    prop   = 0.8, # Proporción del 80%
                    strata = price # Variable respuesta 
                 )
data_train <- training(split_inicial)
data_test  <- testing(split_inicial)

Es importante verificar que la distribución de la variable respuesta es similar en el conjunto de entrenamiento y en el de test. Para asegurar que esto se cumple, la función initial_split() permite identificar con el argumento strata la variable en base a la cual hacer el reparto.

summary(data_train$price)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   5000  145000  189050  212423  259000  775000 
summary(data_test$price)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  20000  145000  189950  210157  257750  620000 

Este tipo de reparto estratificado asegura que el conjunto de entrenamiento y el de test sean similares en cuanto a la variable respuesta, sin embargo, no garantiza que ocurra lo mismo con los predictores. Por ejemplo, en un set de datos con 100 observaciones, un predictor binario que tenga 90 observaciones de un grupo y solo 10 de otro, tiene un alto riesgo de que, en alguna de las particiones, el grupo minoritario no tenga representantes. Si esto ocurre en el conjunto de entrenamiento, algunos algoritmos darán error al aplicarlos al conjunto de test, ya que no entenderán el valor que se les está pasando. Este problema puede evitarse eliminando variables con varianza próxima a cero, lo cual veremos más adelante.

Exclusión de variables con varianza próxima a cero

No se deben incluir en el modelo predictores que contengan un único valor (varianza-cero) ya que no aportan información. Tampoco es conveniente incluir predictores que tengan una varianza próxima a cero, es decir, predictores que toman solo unos pocos valores, de los cuales, algunos aparecen con muy poca frecuencia. El problema con estos últimos es que pueden convertirse en predictores con varianza cero cuando se dividen las observaciones por validación cruzada o bootstrap.

¿Recuerdas que lo vimos en el análisis exploratorio, EDA?

Aquí, también podemos utilizar la función step_nzv() del paquete recipes que identifica predictores potencialmente problemáticos, es decir, aquellos que tiene un único valor o varianza- cero que cumplen dos condiciones:

  • Variación de frecuencia: Es la variación entre la frecuencia del valor más común y la frecuencia del según valor más común. Este ratio tiende a 1 si las frecuencias están equidistribuidas y a valores grandes cuando la refuencia del valor mayoritario supera por mucho al resto (el denominador es un número decimal pequeño). Valor por defecto freq_cut = 95/5.

  • Porcentaje de valores únicos: número de valores únicos divido entre el total de la muestra (multiplicado por 100). Esta valor se acerca a cero cuando la varianza-cero es mayor. Valor por defecto uniqueCut = 10.

Si bien, cuando eliminamos predictores no informativos podría considerarse un paso propio del proceso de selección de predictores, dado que consiste en un filtrado por varianza, tiene que realizarse antes de estandarizar los datos, ya que después, todos los predictores tienen varianza 1.

Estandarización y escalado de variables numéricas

Cuando los predictores son numéricos, la escala en la que se miden, así como la magnitud de su varianza pueden influir en gran medida en el modelo. Muchos algoritmos de machine learning (SVM, redes neuronales, lasso, etc) son sensibles a esto, de forma que, si no se igualan de alguna forma los predictores, aquellos que se midan en una escala mayor o que tengan más varianza dominarán el modelo aunque no sean los que más relación tienen con la variable respuesta. Existen principalmente 2 estrategias para evitarlo:

  • Centrado: consiste en restarle a cada valor la media del predictor al que pertenece. Si los datos están almacenados en un dataframe, el centrado se consigue restándole a cada valor la media de la columna en la que se encuentra. Como resultado de esta transformación, todos los predictores pasan a tener una media de cero, es decir, los valores se centran en torno al origen.

  • Normalización (estandarización): consiste en transformar los datos de forma que todos los predictores estén aproximadamente en la misma escala. Hay dos formas de lograrlo:

    • Normalización Z-score: dividir cada predictor entre su desviación típica después de haber sido centrado, de esta forma, los datos pasan a tener una distribución normal.

    \[ z = \frac{x - \mu}{\sigma} \]

    • Estandarización max-min: transformar los datos de forma que estén dentro del rango [0, 1].

\[ x_{nom}=\frac{x-x_{min}}{x_{max}-x_{min}} \] NOTA: Nunca se debe estandarizar las variables después de ser binarizadas.

Binarización de las variables cualitativas

La binarización consiste en crear nuevas variables dummy con cada uno de los niveles de las variables cualitativas. A este proceso también se le conoce como one hot encoding. Por ejemplo, una variable llamada color que contenga los niveles rojo, verde y azul, se convertirá en tres nuevas variables (color_rojo, color_verde, color_azul), todas con el valor 0 excepto la que coincide con la observación, que toma el valor 1.

Por defecto, la función step_dummy(all_nominal(), -all_outcomes()) binariza todas las variables almacenadas como tipo factor o character, excepto la variable respuesta. Además, elimina uno de los niveles para evitar redundancias. Volviendo al ejemplo anterior, no es necesario almacenar las tres variables, ya que, si color_rojo y color_verde toman el valor 0, la variable color_azul toma necesariamente el valor 1. Si color_rojo o color_verde toman el valor 1, entonces color_azul es necesariamente 0.

El paquete recipes incorpora una amplia variedad de funciones para preprocesar los datos, facilitando el aprendizaje de las transformaciones únicamente con observaciones de entrenamiento, y poder aplicarlas después a cualquier conjunto de datos. La idea detrás de este paquete es la siguiente:

  1. Definir cuál es la variable respuesta, los predictores y el conjunto de datos de entrenamiento, recipe().

  2. Definir todas las transformaciones (escalado, selección, filtrado…) que se desea aplicar, step_().

  3. Aprender los parámetros necesarios para dichas transformaciones con las observaciones de entrenamiento rep().

  4. Aplicar las transformaciones aprendidas a cualquier conjunto de datos juice(), bake().

# Se almacenan en un objeto `recipe` todos los pasos de preprocesado y, finalmente, se aplican a los datos.
transformer <- recipe(
                  formula = price ~ .,
                  data =  data_train
               ) %>%
               step_naomit(all_predictors()) %>%
               step_nzv(all_predictors()) %>%
               step_center(all_numeric(), -all_outcomes()) %>%
               step_scale(all_numeric(), -all_outcomes()) %>%
               step_dummy(all_nominal(), -all_outcomes())

transformer
Data Recipe

Inputs:

Operations:

Removing rows with NA values in all_predictors()
Sparse, unbalanced variable filter on all_predictors()
Centering for all_numeric(), -all_outcomes()
Scaling for all_numeric(), -all_outcomes()
Dummy variables from all_nominal(), -all_outcomes()

Una vez que se ha definido el objeto recipe, con la función prep() se aprenden las transformaciones con los datos de entrenamiento y se aplican a los dos conjuntos con bake().

# Se entrena el objeto recipe
transformer_fit <- prep(transformer)

# Se aplican las transformaciones al conjunto de entrenamiento y de test
data_train_prep <- bake(transformer_fit, new_data = data_train)
data_test_prep  <- bake(transformer_fit, new_data = data_test)

glimpse(data_train_prep)
Rows: 1,380
Columns: 18
$ lotSize                 <dbl> -0.43894231, 0.26607619, 0.48189818, 0.61139137, 12.19383812, -0.51088…
$ age                     <dbl> 3.62268073, 0.11471419, 0.28667334, -0.91704067, -0.50433872, -0.64190…
$ landValue               <dbl> -0.7674967, -0.5829431, -0.3570715, -0.3543170, -0.8363600, -0.9603140…
$ livingArea              <dbl> 0.31203819, -0.96981699, -0.19293506, -0.20588309, -1.69490679, -0.730…
$ pctCollege              <dbl> -0.4558859, -3.2928560, -0.4558859, -0.4558859, -1.4341515, -1.4341515…
$ bedrooms                <dbl> 1.0465989, 1.0465989, -0.1681248, -0.1681248, -1.3828485, -0.1681248, …
$ fireplaces              <dbl> 0.7295147, 0.7295147, -1.0876880, -1.0876880, -1.0876880, -1.0876880, …
$ bathrooms               <dbl> -1.3602353, -1.3602353, -0.6045490, 0.1511373, -1.3602353, -0.6045490,…
$ rooms                   <dbl> 0.425229547, 0.425229547, 0.425229547, -0.440920675, -1.307070897, 0.4…
$ price                   <int> 109000, 120000, 90000, 120000, 85860, 127000, 89900, 60000, 87500, 112…
$ heating_hot.water.steam <dbl> 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1…
$ heating_electric        <dbl> 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ fuel_electric           <dbl> 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ fuel_oil                <dbl> 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ sewer_public.commercial <dbl> 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1…
$ sewer_none              <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ newConstruction_No      <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ centralAir_No           <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1…

Tras el preprocesado de los datos, se han generado un total de 18 variables (17 predictores y la variable respuesta).

Modelado

Ya analizado los datos (entenderlos por medio del EDA), que han sido preprocesados y los predictores seleccionados, el siguiente paso es emplear un algoritmo de machine learning que permita crear un modelo capaz de representar los patrones presentes en los datos de entrenamiento y generalizarlos a nuevas observaciones. Encontrar el mejor modelo no es fácil, existen multitud de algoritmos, cada uno con unas características propias y con distintos parámetros que deben ser ajustados. Por lo general, las etapas seguidas para obtener un buen modelo son:

  • Ajuste/entrenamiento: consiste en aplicar un algoritmo de machine learning a los datos de entrenamiento para que el modelo aprenda.

  • Evaluación/validación: el objetivo de un modelo predictivo no es ser capaz de predecir observaciones que ya se conocen, sino nuevas observaciones que el modelo no ha visto. Para poder estimar el error que comete un modelo es necesario recurrir a estrategias de validación entre las que destacan: validación simple, bootstrap y validación cruzada.

  • Optimización de hiperparámetros: muchos algoritmos de machine learning contienen en sus ecuaciones uno o varios parámetros que no se aprenden con los datos, a estos se les conoce como hiperparámetros. Por ejemplo, los SVM lineal tiene el hiperparámetro de coste C y los árboles de regresión la profundidad del árbol tree_depth y el número mínimo de observaciones por nodo min_n. No existe forma de conocer de antemano cuál es el valor exacto de un hiperparámetro que da lugar al mejor modelo, por lo que se tiene que recurrir a estrategias de validación para comparar distintos valores.

  • Predicción: una vez creado el modelo, este se emplea para predecir nuevas observaciones.

Es a lo largo de todo este proceso donde más destacan las funcionalidades ofrecidas por tidymodels, permitiendo emplear la misma sintaxis para ajustar, optimizar, evaluar y predecir un amplio abanico de modelos, variando únicamente el nombre del algoritmo.

Aunque tidymodels permite todo esto con apenas unas pocas líneas de código, son muchos los argumentos que pueden ser adaptados, cada uno con múltiples posibilidades. Con el objetivo de exponer mejor cada una de las opciones, en lugar de crear directamente un modelo final, se muestran ejemplos de menor a mayor complejidad.

Entrenamiento

Los modelos de tidymodels son accesibles por medio del paquete parsnip. Aunque un mismo modelo puede estar en varios paquetes con nombres distintos.

Para crear un modelo se requieren únicamente tres pasos:

  • Definir el tipo de modelo y sus parámetros

  • Definir qué implementación del modelo se quiere aplicar (engine)

  • Ajustar el modelo

Vamos a ajustar un modelo de árbol de regresión para predecir el precio de la vivienda de Saratoga en función de todos los predictores disponibles.
A excepción de la implementación del algoritmo ("rpart"), rpart (viene por default) o "C5.0" (solo para clasificación), todos los demás argumentos de la función decision_tree() se dejan por defecto.

model_tree <- decision_tree(mode = "regression") %>%
               set_engine(engine = "rpart")
model_tree
Decision Tree Model Specification (regression)

Computational engine: rpart 

Con las líneas anteriores sólo guardamos el tipo de algoritmo (sus parámetros, hiperparámetros y el paquete en el que está implementado). El sigiente paso es ajustar el modelo, para los cual tenemos dos opciones:

  • fit() que emplea como argumento una formula para definir la variable respuesta y los predictores.

  • fit_xy() que emplea los argumentos x y y para definir la matriz de predictores y el vector con la variable respuesta.

# Entrenamiento empleando fórmula
model_tree_fit <- model_tree %>%
                   fit(
                     formula = price ~ .,
                     data    = data_train_prep
                   )
# Entrenamiento empleando x e Y.
variable_respuesta <- "price"

predicores <- setdiff(colnames(data_train_prep), variable_respuesta)

model_tree_fit <- model_tree %>%
                   fit_xy(
                     x = data_train_prep[, predicores],
                     y = data_train_prep[[variable_respuesta]]
                   )

Para ver nuestro modelo utilizamos lo que se almacena dentro de model_tree_fit como fit:

model_tree_fit$fit
n= 1380 

node), split, n, deviance, yval
      * denotes terminal node

  1) root 1380 1.382982e+13 212423.0  
    2) livingArea< 0.3856801 956 3.951824e+12 172884.3  
      4) landValue< -0.3405443 540 1.320059e+12 147526.2  
        8) livingArea< -0.3968666 397 5.752973e+11 135085.3 *
        9) livingArea>=-0.3968666 143 5.127270e+11 182065.0 *
      5) landValue>=-0.3405443 416 1.833788e+12 205801.0  
       10) landValue< 0.8136044 359 1.118047e+12 196236.6  
         20) livingArea< -0.001951587 251 7.002854e+11 182517.3 *
         21) livingArea>=-0.001951587 108 2.607222e+11 228121.3 *
       11) landValue>=0.8136044 57 4.760637e+11 266039.8 *
    3) livingArea>=0.3856801 424 5.013749e+12 301571.5  
      6) landValue< 0.9733672 311 1.944443e+12 268743.7  
       12) livingArea< 1.830195 277 1.304977e+12 256947.8  
         24) centralAir_No>=0.5 140 4.264425e+11 233996.8 *
         25) centralAir_No< 0.5 137 7.294296e+11 280401.5 *
       13) livingArea>=1.830195 34 2.869182e+11 364844.9 *
      7) landValue>=0.9733672 113 1.811733e+12 391920.8  
       14) rooms< 1.940992 89 1.063606e+12 365619.8  
         28) age< -0.8998448 41 9.079364e+10 317584.5 *
         29) age>=-0.8998448 48 7.974026e+11 406650.0  
           58) livingArea< 0.7029069 13 7.632773e+10 320607.7 *
           59) livingArea>=0.7029069 35 5.890850e+11 438608.6  
            118) age>=-0.7622774 27 2.569939e+11 403566.7 *
            119) age< -0.7622774 8 1.870413e+11 556875.0 *
       15) rooms>=1.940992 24 4.582587e+11 489453.6 *

Validación del modelo

Lo que buscamos ver es que tan certera es la predicción de nuestro modelo, ya sea con observaciones que no ha visto el modelo y por ende, con observaciones futuras.

El error mostrado por defecto tras entrenar un modelo suele ser el error de entrenamiento, el error que comete el modelo al predecir las observaciones que ya ha “visto”.

Estos errores son útiles para entender cómo está aprendiendo el modelo (estudio de residuos), no es una estimación realista de cómo se comporta el modelo ante nuevas observaciones (el error de entrenamiento suele ser demasiado optimista).

Para conseguir una estimación más certera, y antes de recurrir al conjunto de test, se pueden emplear estrategias de validación basadas en resampling. El paquete rsampler incorpora los métodos Bootstrap (bootstraps), V-Fold Cross-Validation y Repeated V-Fold Cross-Validation (vfold_cv), Nested or Double Resampling (nested_cv), Group V-Fold Cross-Validation (group_vfold_cv), Leave-One-Out Cross-Validation (loo_cv), Monte Carlo Cross-Validation(mc_cv). Cada uno funciona internamente de forma distinta, pero todos ellos se basan en la idea: ajustar y evaluar el modelo de forma repetida, empleando cada vez distintos subconjuntos creados a partir de los datos de entrenamiento y obteniendo en cada repetición una estimación del error (simulaciones de los datos). El promedio de todas las estimaciones tiende a converger en el valor real del error de test.

Se ajusta de nuevo el modelo, esta vez con validación cruzada repetida para estimar su error.

En primer lugar, se crea un objeto resample que contenga la información sobre las observaciones que forman parte de cada partición. Dado que el reparto es aleatorio, es importante emplear una semilla set.seed() para que los resultados sean reproducibles.

set.seed(1234)
cv_folds <- vfold_cv(
              data    = data_train,
              v       = 5,
              repeats = 10,
              strata  = price
            )
head(cv_folds)

Con la función fit_resamples() el modelo se ajusta y evalúa con cada una de las particiones de forma automática, calculando y almacenando en cada iteración la métrica de interés. Como se comentó anteriormente, cuando se realizan validaciones por resampling, el preprocesado debe ocurrir dentro de cada iteración.

Para seguir este principio, las particiones se crean con el conjunto de datos de entrenamiento sin preprocesar y se le pasa al argumento preprocessor un objeto recipe que contenga la definición del preprocesado.

model_tree <- decision_tree(mode = "regression") %>%
               set_engine(engine = "rpart")

validation_fit <- fit_resamples(
                    object       = model_tree,
                    # El objeto recipe no tiene que estar entrenado
                    preprocessor = transformer,
                    # Las resamples se tienen que haber creado con los datos sin 
                    # prerocesar
                    resamples    = cv_folds,
                    metrics      = metric_set(rmse, mae),
                    control      = control_resamples(save_pred = TRUE)
                  )

head(validation_fit)

Los resultados de fit_resamples() se almacenan en forma de tibble, donde las columnas contienen la información sobre cada partición: su id, las observaciones que forman parte, las métricas calculadas, si ha habido algún error o warning durante el ajuste, y las predicciones de validación si se ha indicado control = control_resamples(save_pred = TRUE). La información puede extraerse haciendo un unnest() de las columnas o empleando las funciones auxiliares collect_predictions() y collect_metrics().

# Métricas promedio de todas las particiones
validation_fit %>% collect_metrics(summarize = TRUE)
# Métricas individuales de cada una de las particiones
validation_fit %>% collect_metrics(summarize = FALSE) %>% 
  head()
library("ggpubr")
Registered S3 method overwritten by 'data.table':
  method           from
  print.data.table     
# Valores de validación (mae y rmse) obtenidos en cada partición y repetición.
p1 <- ggplot(
        data = validation_fit %>% collect_metrics(summarize = FALSE),
        aes(x = .estimate, fill = .metric)) +
      geom_density(alpha = 0.5) +
      theme_bw() 
p2 <- ggplot(
        data = validation_fit %>% collect_metrics(summarize = FALSE),
        aes(x = .metric, y = .estimate, fill = .metric, color = .metric)) +
      geom_boxplot(outlier.shape = NA, alpha = 0.1) +
      geom_jitter(width = 0.05, alpha = 0.3) +
      coord_flip() +
      theme_bw() +
      theme(axis.text.x = element_blank(), axis.ticks.x = element_blank())
  
ggarrange(p1, p2, nrow = 2, common.legend = TRUE, align = "v") %>% 
annotate_figure(
  top = text_grob("Distribución errores de validación cruzada", size = 15)
)

El rmse (raíz del error medio cuadrático) promedio estimado mediante validación cruzada repetida es de 67990. Este valor será contrastado más adelante cuando se calcule el rmse del modelo con el conjunto de test.

Almacenar las predicciones de cada partición es útil para poder evaluar los residuos del modelo y diagnosticar así su comportamiento.

# Predicciones individuales de cada observación.
# Si summarize = TRUE se agregan todos los valores predichos a nivel de
# observación.
validation_fit %>% collect_predictions(summarize = TRUE) %>% head()
p1 <- ggplot(
        data = validation_fit %>% collect_predictions(summarize = TRUE),
        aes(x = price, y = .pred)
      ) +
      geom_point(alpha = 0.3) +
      geom_abline(slope = 1, intercept = 0, color = "firebrick") +
      labs(title = "Valor predicho vs valor real") +
      theme_bw()


p2 <- ggplot(
        data = validation_fit %>% collect_predictions(summarize = TRUE),
        aes(x = .row, y = price - .pred)
      ) +
      geom_point(alpha = 0.3) +
      geom_hline(yintercept =  0, color = "firebrick") +
      labs(title = "Residuos del modelo") +
      theme_bw()

p3 <- ggplot(
        data = validation_fit %>% collect_predictions(summarize = TRUE),
        aes(x = price - .pred)
      ) +
      geom_density() + 
      labs(title = "Distribución residuos del modelo") +
      theme_bw()

p4 <- ggplot(
        data = validation_fit %>% collect_predictions(summarize = TRUE),
        aes(sample = price - .pred)
      ) +
      geom_qq() +
     geom_qq_line(color = "firebrick") +
      labs(title = "Q-Q residuos del modelo") +
      theme_bw()

ggarrange(plotlist = list(p1, p2, p3, p4)) %>%
annotate_figure(
  top = text_grob("Distribución residuos", size = 15, face = "bold")
)

Cuando se aplican métodos de* resampling debemos tener en cuenta dos cosas:

  1. El coste computacional que implica ajustar múltiples veces un modelo, cada vez con un subconjunto de datos distinto,

  2. y la reproducibilidad en la creación de las particiones.

fit_resamples() permite paralelizar el proceso si se registra un backed paralelo, para ello usamos la función resterDoParallel() del paquete doParallel.

library("doParallel")
Loading required package: foreach
Warning: package ‘foreach’ was built under R version 4.0.5
Attaching package: ‘foreach’

The following objects are masked from ‘package:purrr’:

    accumulate, when

Loading required package: iterators
Loading required package: parallel
registerDoParallel(cores = detectCores() - 1)
set.seed(2020)
model_tree <- decision_tree(mode = "regression") %>%
               set_engine(engine = "rpart")

validacion_fit <- fit_resamples(
                    object       = model_tree,
                    preprocessor = transformer,
                    resamples    = cv_folds,
                    metrics      = metric_set(rmse, mae),
                    control      = control_resamples(save_pred = TRUE)
                  )

stopImplicitCluster()

Hiperparámetros (tuning)

Muchos modelos, entre ellos los árboles de regresión, contienen parámetros (datos de entrada al modelo) que no pueden aprenderse a partir de los datos de entrenamiento y, por lo tanto, deben de ser establecidos por el analista. A estos se les conoce como hiperparámetros. ¡Ojo! no se trata de tener un modelo y correrlo y ya con esto llamarnos científicos de datos.

Los resultados de un modelo pueden depender en gran medida del valor que tomen sus hiperparámetros, sin embargo, no se puede conocer de antemano cuál es el adecuado. Aunque con la práctica, los especialistas en machine learning ganan intuición sobre qué valores pueden funcionar mejor en cada problema, no hay reglas fijas. La forma más común de encontrar los valores óptimos es probando diferentes posibilidades por medio de algoritmos de optimización.

Podemos seguir los siguientes pasos:

  1. Escoger un conjunto de valores para el o los hiperparámetros.

  2. Para cada valor (combinación de valores si hay más de un hiperparámetro), entrenar el modelo y estimar su error mediante un método de validación.

  3. Ajustar de nuevo el modelo, esta vez con todos los datos de entrenamiento y con los mejores hiperparámetros encontrados.

El modelo decision_tree empleado hasta ahora tienen tres hiperparámetros: cost_complexity, tree_depth y min_n. Todos ellos controlan el tamaño del árbol final. Por ejemplo, cuanto mayor el valor de tree_depth, más ramificaciones tiene el árbol y más flexible es el modelo. Esto le permite ajustarse mejor a las observaciones de entrenamiento pero con el riesgo de overfitting. Con las funciones tune() y tune_grid() del paquete tune se pueden explorar diferentes valores de hiperparámetros sin apenas tener que cambiar la estructura del código.

Se vuelve a ajustar un modelo decision_tree con diferentes valores de tree_depth y min_n, y empleando validación cruzada repetida para identificar con cuáles de ellos se obtienen mejores resultados.

# Definición del modelo y los hiperparámetros a optimizar
model_tree <- decision_tree(
                 mode       = "regression",
                 tree_depth = tune(),
                 min_n      = tune()
               ) %>%
               set_engine(engine = "rpart")

# Validación y creación de particiones
set.seed(1234)
cv_folds <- vfold_cv(
              data    = data_train,
              v       = 5,
              strata  = price
             )

# Optimización de hiperparámetros
registerDoParallel(cores = parallel::detectCores() - 1)

grid_fit <- tune_grid(
              object       = model_tree,
              # El objeto recipe no tiene que estar entrenado
              preprocessor = transformer,
              # Las resamples se tienen que haber creado con los datos sin 
              # prerocesar
              resamples    = cv_folds,
              metrics      = metric_set(rmse, mae),
              control      = control_grid(save_pred = TRUE),
              # Número de combinaciones generadas automáticamente
              grid         = 70
            )
stopImplicitCluster()

Los resultados de la búsqueda de hiperparámetros pueden verse haciendo un unnest() de el tibble que se crea de la ejecución grid_fit, o con las funciones auxiliares collect_metrics(), collect_predictions, show_best() y select_best().

grid_fit %>% unnest(.metrics) %>% head()
grid_fit %>% collect_metrics(summarize = TRUE) %>% head()
grid_fit %>% show_best(metric = "rmse", n = 5)

Es conveniente visualizar la evolución del error en función de los hiperparámetros. Sin embargo, hay que tener en cuenta que estos no son independientes los unos de los otros, el comportamiento de un hiperparámetro puede cambiar mucho dependiendo del valor que tomen los otros.

grid_fit %>%
  collect_metrics(summarize = TRUE) %>%
  filter(.metric == "rmse") %>%
  select(-c(.estimator, n)) %>%
  pivot_longer(
    cols = c(tree_depth, min_n),
    values_to = "value",
    names_to = "parameter"
  ) %>%
  ggplot(aes(x = value, y = mean, color = parameter)) +
  geom_point() +
  geom_line() + 
  labs(title = "Evolución del error en función de los hiperparámetros") +
  facet_wrap(facets = vars(parameter), nrow = 2, scales = "free") +
  theme_bw() + 
  theme(legend.position = "none")

grid_fit %>%
  collect_metrics(summarize = TRUE) %>%
  filter(.metric == "rmse") %>%
  select(-c(.estimator, n)) %>%
  ggplot(aes(x = tree_depth, y = min_n, color = mean, size = mean)) +
  geom_point() +
  scale_color_viridis_c() +
  labs(title = "Evolución del error en función de los hiperparámetros") +
  theme_bw()

En este caso, parece que el error del modelo se reduce a medida que el valor de min_n aumenta (aunque no es estable) y también el de tree_depth. En este último la mejora parece estabilizarse a partir de 4.

Aunque los autores de tidymodels han establecido por defecto valores adecuados basados en su amplia experiencia, en muchos casos es conveniente tener más control sobre la búsqueda, ya que la selección se hace de forma automática. Para conseguir este control, podemos utilizar expand.grid() (para especificar exactamente qué valores se emplean), o con las funciónes regular_grid(), grid_random(), grid_max_entropy(), y grid_latin_hypercube() del paquete dials.

Se repite la búsqueda de mejores hiperparámetros pero esta vez definiendo el espacio de búsqueda.

# Definición del modelo y de los hiperparámetros a optimizar
model_tree <- decision_tree(
                 mode       = "regression",
                 tree_depth = tune(),
                 min_n      = tune()
               ) %>%
               set_engine(engine = "rpart")

# Validación y creación de particiones
set.seed(1234)
cv_folds <- vfold_cv(
              data    = data_train,
              v       = 5,
              strata  = price
             )

# Grid de Hiperparámetros
set.seed(1234)
hiperpar_grid <- grid_random(
                  # Rango de búsqueda para cada hiperparámetro
                  tree_depth(range = c(1, 10), trans = NULL),
                  min_n(range      = c(2, 100), trans = NULL),
                  # Número combinaciones aleatorias probadas
                  size = 50
                )

# Ejecución y optimización de hiperparámetros
registerDoParallel(cores = parallel::detectCores() - 1)

grid_fit <- tune_grid(
              object       = model_tree,
              # El objeto recipe no tiene que estar entrenado
              preprocessor = transformer,
              # Las resamples se tienen que haber creado con los datos sin 
              # prerocesar
              resamples    = cv_folds,
              metrics      = metric_set(rmse, mae),
              control      = control_resamples(save_pred = TRUE),
              # Hiperparámetros
              grid         = hiperpar_grid
            )
stopImplicitCluster()

Ahora vemos los resultados:

grid_fit %>% show_best(metric = "rmse", n = 5)
grid_fit %>%
  collect_metrics(summarize = TRUE) %>%
  filter(.metric == "rmse") %>%
  select(-c(.estimator, n)) %>%
  pivot_longer(
    cols = c(tree_depth, min_n),
    values_to = "value",
    names_to = "parameter"
  ) %>%
  ggplot(aes(x = value, y = mean, color = parameter)) +
  geom_point() +
  geom_line() + 
  labs(title = "Evolución del error en función de los hiperparámetros") +
  facet_wrap(facets = vars(parameter), nrow = 2, scales = "free") +
  theme_bw() + 
  theme(legend.position = "none")

Los mejores resultados, en base al rmse se han obtenido con los hiperparámetros:

# Selección de los mejores hiperparámetros encontrados
best_hip <- select_best(grid_fit, metric = "rmse")
best_hip

Modelo final

Ya haciendo la optimización de los hiperparámetros, entramos al modelo pero con los datos de entrenamiento con estos parámetros.

final_model_tree <- finalize_model(x = model_tree, 
                                   parameters = best_hip)
final_model_tree
Decision Tree Model Specification (regression)

Main Arguments:
  tree_depth = 10
  min_n = 69

Computational engine: rpart 
final_model_tree_fit  <- final_model_tree %>%
                          fit(
                            formula = price ~ .,
                            data    = data_train_prep
                          )

Predicción

Una vez que el modelo final ha sido ajustado, con la función predict() se pueden predecir nuevos datos. Los argumentos de la función son:

  • object: un modelo entrenado.

  • newdata: un dataframe con nuevas observaciones.

  • type: tipo de predicción ("numeric", "class", "prob", "conf_int", "pred_int", "quantile", or "raw").

predicciones <- final_model_tree_fit  %>%
                predict(
                  new_data = data_test_prep,
                  #new_data = bake(transformer_fit, datos_test),
                  type = "numeric"
                )
predicciones %>% head()

Error de test

Ya hemos validado con modelos de validación (CV, bootstrapping…) y se han conseguido estimaciones del error del modelo al predecir nuevas observaciones. Pero la mejor forma de evaluar el modelo final es prediciendo un conjunto test, es decir, utilizando un conjunto de datos que no hemos usado en el entrenamiento y en la optimización. El paquete yardstick tiene funciones predefinidas para métricas en cuestión que nos pueden ayudar.

predicciones <- predicciones %>% 
                bind_cols(data_test_prep %>% select(price))

rmse(predicciones, truth = price, estimate = .pred, na_rm = TRUE)

En el apartado de optimización, se estimó, mediante validación cruzada repetida, que el rmse del modelo model_tree era de 67790, un valor próximo al obtenido con el conjunto de test baja a 61536.

Workflows

Los workflows permiten combinar en un sólo objeto todos los elementos que se encargan del procesamiento (recipes), modelado (parsnip y tune) y post-procesado. Para crear el workflows se van encadenando los elementos con las funciones add_* o modificando los elementos de las funciones update_*.
# Definición del modelo y los hiperparámetros a optimizar
model_tree <- decision_tree(
                 mode       = "regression",
                 tree_depth = tune(),
                 min_n      = tune()
               ) %>%
               set_engine(engine = "rpart")

# Definición de procesado
transformer <- recipe(
                  formula = price ~ .,
                  data =  data_train
               ) %>%
               step_naomit(all_predictors()) %>%
               step_nzv(all_predictors()) %>%
               step_center(all_numeric(), -all_outcomes()) %>%
               step_scale(all_numeric(), -all_outcomes()) %>%
               step_dummy(all_nominal(), -all_outcomes())

# Estrategia de validación y creación de particiones
set.seed(1234)
cv_folds <- vfold_cv(
              data    = data_train,
              v       = 5,
              strata  = price
             )

# Workflow
workflow_modelado <- workflow() %>%
                     add_recipe(transformer) %>%
                     add_model(model_tree)

workflow_modelado
══ Workflow ════════════════════════════════════════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: decision_tree()

── Preprocessor ────────────────────────────────────────────────────────────────────────────────────────
5 Recipe Steps

• step_naomit()
• step_nzv()
• step_center()
• step_scale()
• step_dummy()

── Model ───────────────────────────────────────────────────────────────────────────────────────────────
Decision Tree Model Specification (regression)

Main Arguments:
  tree_depth = tune()
  min_n = tune()

Computational engine: rpart 

El objeto workflow puede ajustarse directamente con la función fit() o hacer tuning de hiperparámetros con tune_grid().

# Grid de hiperparámetros
hiperpar_grid <- grid_regular(
                  # Rango de búsqueda para cada hiperparámetro
                  tree_depth(range = c(1, 10), trans = NULL),
                  min_n(range = c(2, 100), trans = NULL),
                  # Número valores por hiperparámetro
                  levels = c(3, 3)
                )

# Ejecución optimización hiperparámetros
registerDoParallel(cores = parallel::detectCores() - 1)

grid_fit <- tune_grid(
              object       = workflow_modelado,
              resamples    = cv_folds,
              metrics      = metric_set(rmse, mae),
              control      = control_resamples(save_pred = TRUE),
              # Hiperparámetros
              grid         = hiperpar_grid
            )
stopImplicitCluster()

# Entrenamiento final
best_hiper <- select_best(grid_fit, metric = "rmse")

final_model_fit <- finalize_workflow(
                        x = workflow_modelado,
                        parameters = best_hiper
                    ) %>%
                    fit(
                      data = data_train
                    ) %>%
                    pull_workflow_fit()

Estrategias de tuning

Uno de los problemas que tenemos para encontrar los mejores hiperparámetros es el coste computacional. Una alternativa es la búsqueda de hiperparámetros con métodos de optimización bayesiana.

En términos generales, la optimización bayesiana de hiperparámetros consiste en crear un modelo probabilístico en el que la función objetivo es la métrica de validación del modelo (rmse, auc, precisión, etc). Con esta estrategia, se consigue que la búsqueda se vaya redirigiendo en cada iteración hacia las regiones de mayor interés.

El objetivo final es reducir el número de combinaciones de hiperparámetros con las que se evalúa el modelo, eligiendo únicamente los mejores candidatos. Esto significa que, la ventaja frente a las otras estrategias mencionadas, se maximiza cuando el espacio de búsqueda es muy amplio o la evaluación del modelo es muy lenta.

# Definición del modelo y los hiperparámetros a optimizar
model_tree <- decision_tree(
                 mode       = "regression",
                 tree_depth = tune(),
                 min_n      = tune()
                ) %>%
               set_engine(engine = "rpart")

# Procesado
transformer <- recipe(
                  formula = price ~ .,
                  data =  data_train
               ) %>%
               step_naomit(all_predictors()) %>%
               step_nzv(all_predictors()) %>%
               step_center(all_numeric(), -all_outcomes()) %>%
               step_scale(all_numeric(), -all_outcomes()) %>%
               step_dummy(all_nominal(), -all_outcomes())

# Estrategia de validación y creación de particiones
set.seed(1234)
cv_folds <- vfold_cv(
              data    = data_train,
              v       = 5,
              strata  = price
             )

# workflow
workflow_modelado <- workflow() %>%
                     add_recipe(transformer) %>%
                     add_model(model_tree)

# Grid de hiperparámetros
hiperpar_grid <- grid_regular(
                  # Rango de búsqueda para cada hiperparámetro
                  tree_depth(range = c(1, 10), trans = NULL),
                  min_n(range = c(2, 100), trans = NULL),
                  # Número valores por hiperparámetro
                  levels = c(3, 3)
                )

# Optimización de hiperparámetros
registerDoParallel(cores = parallel::detectCores() - 1)

grid_fit <- tune_bayes(
              workflow_modelado, 
              resamples = cv_folds,
              # Iniciación aleatoria con 20 candidatos
              initial = 20,
              # Numero de iteraciones de optimización
              iter    = 30,
              # Métrica optimizada
              metrics = metric_set(rmse),
              control = control_bayes(no_improve = 20, verbose = FALSE)
            )
! No improvement for 20 iterations; returning current results.
stopImplicitCluster()

Al igual que con el método anterior, podemos ver la evolución del error:

autoplot(grid_fit, type = "performance") +
  labs(title = "Evolución del error") +
  theme_bw()

Para mostrar los mejores hiperparámetros encontrados

show_best(x = grid_fit, metric = "rmse")

Nótese que incluso el rmse encontrado es más bajo que con el caso anterior. Por último realizamos el entrenamiento final:

best_hiper <- select_best(grid_fit, metric = "rmse")

final_model_fit <- finalize_workflow(
                        x = workflow_modelado,
                        parameters = best_hiper
                    ) %>%
                    fit(
                      data = data_train
                    ) %>%
                    pull_workflow_fit()

Y evaluamos las predicciones

predicciones <- final_model_tree_fit  %>%
                predict(
                  new_data = data_test_prep,
                  #new_data = bake(transformer_fit, datos_test),
                  type = "numeric"
                )
predicciones %>% head()

Y por último encontramos el error de test:

predicciones <- predicciones %>% 
                bind_cols(data_test_prep %>% select(price))

rmse(predicciones, truth = price, estimate = .pred, na_rm = TRUE)

Podemos llegar al mismo reslultado anterior de 61536 con un coste computacional menor.

Modelos

En esta sección veremos modelos disponibles en la paquetería parsnip y al final comparamos sus resultados, estos modelos son:

  • GLM (Generalized Linear Models),

  • Random forest,

  • SVM (Suport Vector Machine), y

  • MARS (Multivariate adaptive regression splines).

GLM

Realizaremos el mismo procedimiento que hicimos con árboles de decisión, pero ahora cambiando al modelo de regresión lineal:

# Definición del modelo y los hiperparámetros (GLM)
model_glm <- linear_reg(
                 mode    = "regression",
                 penalty = tune(),
                 mixture = tune()
              ) %>%
              set_engine(engine = "glmnet", nlambda = 100)

# Procesado
transformer <- recipe(
                  formula = price ~ .,
                  data =  data_train
               ) %>%
               step_naomit(all_predictors()) %>%
               step_nzv(all_predictors()) %>%
               step_center(all_numeric(), -all_outcomes()) %>%
               step_scale(all_numeric(), -all_outcomes()) %>%
               step_dummy(all_nominal(), -all_outcomes())

# Validación y generación de particiones
set.seed(1234)
cv_folds <- vfold_cv(
              data    = data_train,
              v       = 5,
              strata  = price
             )

# Workflow
workflow_modelado <- workflow() %>%
                     add_recipe(transformer) %>%
                     add_model(model_glm)

# Grid de hiperparámetros
hiperpar_grid <- grid_regular(
                  # Rango de búsqueda para cada hiperparámetro
                  penalty(range = c(0, 1), trans = NULL),
                  mixture(range = c(0, 1), trans = NULL),
                  # Número de combinaciones totales
                  levels = c(10, 10)
                )

# Optimización de hiperparámetros
registerDoParallel(cores = parallel::detectCores() - 1)
grid_fit <- tune_grid(
              object    = workflow_modelado,
              resamples = cv_folds,
              metrics   = metric_set(rmse),
              control   = control_resamples(save_pred = TRUE),
              # Hiperparámetros
              grid      = hiperpar_grid
            )
stopImplicitCluster()

Mostramos los mejores resultados de la optimización:

show_best(grid_fit, metric = "rmse", n = 10)

Usamos los hiperparámetros óptimos para obtener el modelo final:

best_hiper <- select_best(grid_fit, metric = "rmse")

modelo_glm <- finalize_workflow(
                x = workflow_modelado,
                parameters = best_hiper
              )

modelo_glm_fit <-  modelo_glm %>%
                   fit(
                     data = data_train
                   )

Calculamos las predicciones con el test

predicciones <- modelo_glm_fit %>%
                predict(
                  new_data = data_test,
                  type = "numeric"
                )

Y por último calculamos el error

predicciones <- predicciones %>% 
                bind_cols(data_test_prep %>% select(price))

error_test_glm  <- rmse(
                     data     = predicciones,
                     truth    = price,
                     estimate = .pred,
                     na_rm    = TRUE
                   ) %>%
                   mutate(
                     modelo = "GLM"
                   )
error_test_glm

Random Forest

Un modelo Random Forest está formado por un conjunto de árboles de decisión individuales, cada uno ajustado empleando una muestra bootstrapping de los datos de entrenamiento.

En el entrenamiento de cada árbol, las observaciones se van distribuyendo por nodos generando la estructura del árbol hasta alcanzar un nodo terminal. Cuando se quiere predecir una nueva observación, esta recorre el árbol acorde al valor de sus predictores hasta alcanzar uno de los nodos terminales. La predicción del árbol es la media de la variable respuesta (la moda en problemas de clasificación) de todas las observaciones de entrenamiento que están en este mismo nodo terminal. La predicción de un modelo Random Forest es la media de las predicciones de todos los árboles que lo forman.

Encontremos el modelo utilizando random forest:

# Definición del modelo y los hiperparámetros a optimizar
modelo_rf <- rand_forest(
                 mode  = "regression",
                 mtry  = tune(),
                 trees = tune(),
                 min_n = tune()
              ) %>%
              set_engine(engine = "ranger")

# Procesado
transformer <- recipe(
                  formula = price ~ .,
                  data =  data_train
               ) %>%
               step_naomit(all_predictors()) %>%
               step_nzv(all_predictors()) %>%
               step_center(all_numeric(), -all_outcomes()) %>%
               step_scale(all_numeric(), -all_outcomes()) %>%
               step_dummy(all_nominal(), -all_outcomes())

# Estrategia de validación y creación de particiones
set.seed(1234)
cv_folds <- vfold_cv(
              data    = data_train,
              v       = 5,
              strata  = price
             )

# Workflow
workflow_modelado <- workflow() %>%
                     add_recipe(transformer) %>%
                     add_model(modelo_rf)

# Grid de hiperparámetros
hiperpar_grid <- grid_max_entropy(
                  # Rango de búsqueda para cada hiperparámetro
                  mtry(range = c(1L, 10L), trans = NULL),
                  trees(range = c(500L, 3000L), trans = NULL),
                  min_n(range = c(2L, 100L), trans = NULL),
                  # Número de combinaciones totales
                  size = 100
                )

# Optimización de hiperparámetros
registerDoParallel(cores = parallel::detectCores() - 1)
grid_fit <- tune_grid(
              object    = workflow_modelado,
              resamples = cv_folds,
              metrics   = metric_set(rmse),
              control   = control_resamples(save_pred = TRUE),
              # Hiperparámetros
              grid      = hiperpar_grid
            )
stopImplicitCluster()

Mostramos los resultados de optimización:

show_best(grid_fit, metric = "rmse")

Usamos los mejores hiperparámetros en el modelo:

best_hiper <- select_best(grid_fit, metric = "rmse")

modelo_rf <- finalize_workflow(
                x = workflow_modelado,
                parameters = best_hiper
             )

modelo_rf_fit <- modelo_rf %>%
                 fit(
                  data = data_train
                 )

Evaluamos las predicciones de test

predicciones <- modelo_rf_fit %>%
                predict(
                  new_data = data_test,
                  type     = "numeric"
                )

y encontramos el error

predicciones <- predicciones %>% 
                bind_cols(data_test_prep %>% select(price))

error_test_rf  <- rmse(
                     data     = predicciones,
                     truth    = price,
                     estimate = .pred,
                     na_rm    = TRUE
                   ) %>%
                   mutate(
                     modelo = "RF"
                   )
error_test_rf

SVM

El método de clasificación-regresión Suport Vector Machine (SVM) es un método de no paramétrico, se fundamenta en el concepto de hiperplano y de máxima separación. Los modelos SVM encuentran el hiperplano óptimo capaz de predecir el valor/clasificación de las observaciones con el menor error posible.

Para que el modelo nos funcione correctamente debemos instalar el paquete kernlab.

# Definición del modelo y los hiperparámetros a optimizar
modelo_svm <- svm_rbf(
                 mode      = "regression",
                 cost      = tune(),
                 rbf_sigma = tune(),
                 margin    = tune()
              ) %>%
              set_engine(engine = "kernlab")

# Procesado
transformer <- recipe(
                  formula = price ~ .,
                  data =  data_train
               ) %>%
               step_naomit(all_predictors()) %>%
               step_nzv(all_predictors()) %>%
               step_center(all_numeric(), -all_outcomes()) %>%
               step_scale(all_numeric(), -all_outcomes()) %>%
               step_dummy(all_nominal(), -all_outcomes())

# Estrategia de validación y creación de particiones
set.seed(1234)
cv_folds <- vfold_cv(
              data    = data_train,
              v       = 5,
              strata  = price
             )

# Workflow
workflow_modelado <- workflow() %>%
                     add_recipe(transformer) %>%
                     add_model(modelo_svm)

# Grid hiperparámetros
hiperpar_grid <- grid_random(
                  # Rango de búsqueda para cada hiperparámetro
                  cost(range = c(-10, -1), trans = log2_trans()),
                  rbf_sigma(range = c(-10, 0), trans = log10_trans()),
                  svm_margin(range = c(0, 0.2), trans = NULL), 
                  # Número de combinaciones totales
                  size = 100
                )

# Optimización de hiperparámetros
registerDoParallel(cores = parallel::detectCores() - 1)
grid_fit <- tune_grid(
              object    = workflow_modelado,
              resamples = cv_folds,
              metrics   = metric_set(rmse),
              control   = control_resamples(save_pred = TRUE),
              # Hiperparámetros
              grid      = hiperpar_grid
            )
stopImplicitCluster()

Mostramos los resultados de la optimización

show_best(grid_fit, metric = "rmse", n = 10)

Y utilizamos los hiperparámetros óptimos para el modelo

best_hiper <- select_best(grid_fit, metric = "rmse")

modelo_svm <- finalize_workflow(
                  x = workflow_modelado,
                  parameters = best_hiper
              )

modelo_svm_fit <- modelo_svm %>%
                  fit(
                    data = data_train
                  )

Encontramos las predicciones en el test:

predicciones <- modelo_svm_fit %>%
                predict(
                  new_data = data_test,
                  type     = "numeric"
                )

Y finalmente encontramos el error

predicciones <- predicciones %>% 
                bind_cols(data_test_prep %>% select(price))

error_test_svm <- rmse(
                     data     = predicciones,
                     truth    = price,
                     estimate = .pred,
                     na_rm    = TRUE
                   ) %>%
                   mutate(
                     modelo = "SVM"
                   )
error_test_svm

MARS

Multivariate adaptive regression splines (MARS) es una extensión no paramétrica de los modelos de regresión lineal que automatiza la incorporación de relaciones no lineales entre los predictores y la variable respuesta, así como interacciones entre los predictores.

Para que este funcione bien debemos instalar el paquete earth.

# Definición del modelo y los hiperparámetros a optimizar
modelo_mars <- mars(
                 mode  = "regression",
                 num_terms = tune(),
                 prod_degree = 2,
                 prune_method = "none"
               ) %>%
               set_engine(engine = "earth")

# Procesado
transformer <- recipe(
                  formula = price ~ .,
                  data =  data_train
               ) %>%
               step_naomit(all_predictors()) %>%
               step_nzv(all_predictors()) %>%
               step_center(all_numeric(), -all_outcomes()) %>%
               step_scale(all_numeric(), -all_outcomes()) %>%
               step_dummy(all_nominal(), -all_outcomes())

# Validación y creación de particiones
set.seed(1234)
cv_folds <- vfold_cv(
              data    = data_train,
              v       = 5,
              strata  = price
             )

# Workflow
workflow_modelado <- workflow() %>%
                     add_recipe(transformer) %>%
                     add_model(modelo_mars)

# Grid de hiperparámetros
hiperpar_grid <- grid_regular(
                  # Rango de búsqueda para cada hiperparámetro
                  num_terms(range = c(1, 20), trans = NULL),
                  levels = 20
                )

# Optimización de hiperparámetros
registerDoParallel(cores = parallel::detectCores() - 1)
grid_fit <- tune_grid(
              object    = workflow_modelado,
              resamples = cv_folds,
              metrics   = metric_set(rmse),
              control   = control_resamples(save_pred = TRUE),
              # Hiperparámetros
              grid      = hiperpar_grid
            )
stopImplicitCluster()

Mostramos los resultados de la optimización

show_best(grid_fit, metric = "rmse", n = 10)

Utilizamos en el modelo los hiperparámetros óptimos:

best_hiper <- select_best(grid_fit, metric = "rmse")

modelo_mars <- finalize_workflow(
                  x = workflow_modelado,
                  parameters = best_hiper
               )

mmodelo_mars_fit <- modelo_svm %>%
                    fit(
                      data = data_train
                    )

Encontramos las predicciones test:

predicciones <- mmodelo_mars_fit %>%
                predict(
                  new_data = data_test,
                  type     = "numeric"
                )

Encontramos el error

predicciones <- predicciones %>% 
                bind_cols(data_test_prep %>% select(price))

error_test_mars <- rmse(
                      data     = predicciones,
                      truth    = price,
                      estimate = .pred,
                      na_rm    = TRUE
                   ) %>%
                    mutate(
                      modelo = "MARS"
                    )
error_test_mars

Comparación

Error de validación cruzada

set.seed(1234)
cv_folds <- vfold_cv(
              data    = data_train,
              v       = 5,
              repeats = 5,
              strata  = price
            )


registerDoParallel(cores = parallel::detectCores() - 1)

validacion_glm <- fit_resamples(
                    object       = modelo_glm,
                    # preprocessor = transformer,
                    resamples    = cv_folds,
                    metrics      = metric_set(rmse),
                    control      = control_resamples(save_pred = FALSE)
                  ) %>%
                  collect_metrics(summarize = FALSE) %>%
                  mutate(modelo = "GLM")

validacion_svm <- fit_resamples(
                    object        = modelo_svm,
                    #preprocessor = transformer,
                    resamples     = cv_folds,
                    metrics       = metric_set(rmse),
                    control       = control_resamples(save_pred = FALSE)
                  ) %>%
                  collect_metrics(summarize = FALSE) %>%
                  mutate(modelo = "SVM")

validacion_rf <- fit_resamples(
                    object       = modelo_rf,
                    # preprocessor = transformer,
                    resamples    = cv_folds,
                    metrics      = metric_set(rmse),
                    control      = control_resamples(save_pred = FALSE)
                  ) %>%
                  collect_metrics(summarize = FALSE) %>%
                  mutate(modelo = "RF")

validacion_mars <- fit_resamples(
                     object       = modelo_mars,
                     # preprocessor = transformer,
                     resamples    = cv_folds,
                     metrics      = metric_set(rmse),
                     control      = control_resamples(save_pred = FALSE)
                   ) %>%
                   collect_metrics(summarize = FALSE) %>%
                   mutate(modelo = "MARS")

stopImplicitCluster()
bind_rows(
  validacion_glm,
  validacion_rf,
  validacion_svm,
  validacion_mars
) %>%
ggplot(aes(x = modelo, y = .estimate, color = modelo)) +
  geom_violin() +
  geom_boxplot(outlier.shape = NA, width = 0.2) +
  geom_point(alpHa = 0.1) +
  labs(title = "Error de validación cruzada", y = "RMSE") +
  theme_bw() +
  theme(legend.position = "none")
Warning: Ignoring unknown parameters: alpHa

Error de test

errores_test <- bind_rows(
                  error_test_glm,
                  error_test_rf,
                  error_test_svm,
                  error_test_mars
                )

errores_test %>% select(modelo, .metric, .estimate)
ggplot(data = errores_test) +
geom_col(aes(x = modelo, y = .estimate, fill= modelo), color = "gray") +
coord_flip() +
labs(title = "Error de test") +
theme_bw()

Referencias

  • Kuhn et al., (2020). Tidymodels: a collection of packages for modeling and machine learning using tidyverse principles. https://www.tidymodels.org

  • G. James, D. Witten, T. Hastie, R. Tibshirani. An Introduction to Statistical Learning. MIT Press, 2010.

  • Linear Models with R By Julian J. Faraway

  • Machine Learning con R y tidymodels, Joaquín Amat Rodrigo.

LS0tCnRpdGxlOiAiQmxvcXVlIDE6IE1vZGVsb3MgZGUgYHRpZHltb2RlbHNgIgpzdWJ0aXRsZTogIkFuYWzDrXRpY2EgYmFzYWRhIGVuIMOhcmJvbGVzIGRlIGNsYXNpZmljYWNpw7NuIHkgcmVncmVzacOzbiIKYXV0aG9yOiAiUHJvZmVzb3JhOiBEcmEuIERpYW5hIFBhb2xhIE1vbnRveWEgRXNjb2JhciBkaWFuYS5tb250b3lhQGl0ZXNvLm14IgpkYXRlOiAiRW5lcm8gMjAyMyIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB5ZXMKICAgIHRoZW1lOiBjb3NtbwogICAgaGlnaGxpZ2h0OiB0YW5nbwogIGdpdGh1Yl9kb2N1bWVudDoKICAgIHRvYzogeWVzCiAgICBkZXY6IGpwZWcKLS0tCmBgYHtyIHNldHVwLCBlY2hvID0gRkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvPSBUUlVFLAogICAgICAgICAgICAgICAgICAgICAgZmlnLmhlaWdodCA9IDYsIGZpZy53aWR0aCA9IDcpCmBgYAoKPHN0eWxlPgouZm9yY2VCcmVhayB7IC13ZWJraXQtY29sdW1uLWJyZWFrLWFmdGVyOiBhbHdheXM7IGJyZWFrLWFmdGVyOiBjb2x1bW47IH0KPC9zdHlsZT4KCjxjZW50ZXI+CiFbXSguL2ltYWdlcy9kcnlfdHJlZS5wbmcpe3dpZHRoPTEwJX0KIVtdKC4vaW1hZ2VzL2l0ZXNvLmpwZWcpe3dpZHRoPTUlfQoKCjwvY2VudGVyPgoKIyBNb2RlbG8KCkVsIG9iamV0aXZvIGRlIHVuICptb2RlbG8qIGVzIHByb3BvcmNpb25hciB1biByZXN1bWVuIHNpbXBsZSBkZSBiYWphIGRpbWVuc2nDs24gZGUgdW4gY29uanVudG8gZGUgZGF0b3MuIElkZWFsbWVudGUsIGVsIG1vZGVsbyBjYXB0dXJhcsOhICBwYXRyb25lcyBnZW5lcmFkb3MgcG9yIGVsIGZlbsOzbWVubyBkZSBpbnRlcsOpcyBlIGlnbm9yYXLDoSBlbCAicnVpZG8iIChlcyBkZWNpciwgbGEgdmFyaWFjacOzbiBhbGVhdG9yaWEgcXVlIG5vIGxlIGludGVyZXNhKS4KClRyYWRpY2lvbmFsbWVudGUsIGVsIGVuZm9xdWUgZGVsIG1vZGVsYWRvIGVzdMOhIGVuIGxhICppbmZlcmVuY2lhKiBvIGNvbmZpcm1hciBxdWUgdW5hIGhpcMOzdGVzaXMgZXMgY2llcnRhLiBIYXkgdW4gcGFyIGRlIGlkZWFzIHF1ZSBub3MgcHVlZGVuIGF5dWRhciBhIGhhY2VyIGluZmVyZW5jaWFzIGNvcnJlY3RhbWVudGU6CgoxLiBDYWRhIG9ic2VydmFjacOzbiBwdWVkZSB1c2Fyc2UgcGFyYSBleHBsb3JhY2nDs24gbyBjb25maXJtYWNpw7NuLCBubyBhbWJhcy4KCjIuIFB1ZWRlIHV0aWxpemFyIHVuYSBvYnNlcnZhY2nDs24gdGFudGFzIHZlY2VzIGNvbW8gZGVzZWUgcGFyYSBsYSBleHBsb3JhY2nDs24sIHBlcm8gc29sbyBwdWVkZSB1dGlsaXphcmxhIHVuYSB2ZXogcGFyYSBsYSBjb25maXJtYWNpw7NuLiBUYW4gcHJvbnRvIGNvbW8gdXRpbGljZSB1bmEgb2JzZXJ2YWNpw7NuIGRvcyB2ZWNlcywgcGFzYXLDoSBkZSBsYSBjb25maXJtYWNpw7NuIGEgbGEgZXhwbG9yYWNpw7NuLgoKRXN0byBlcyBuZWNlc2FyaW8gcG9ycXVlIHBhcmEgY29uZmlybWFyIHVuYSBoaXDDs3Rlc2lzIGRlYmUgdXRpbGl6YXIgZGF0b3MgaW5kZXBlbmRpZW50ZXMgZGUgbG9zIGRhdG9zIHF1ZSB1dGlsaXrDsyBwYXJhIGdlbmVyYXIgbGEgaGlww7N0ZXNpcy4gRGUgbG8gY29udHJhcmlvLCBzZXLDoSBkZW1hc2lhZG8gb3B0aW1pc3RhLiBObyBoYXkgYWJzb2x1dGFtZW50ZSBuYWRhIGRlIG1hbG8gZW4gbGEgZXhwbG9yYWNpw7NuLCBwZXJvICpudW5jYSBkZWJlIHZlbmRlciB1biBhbsOhbGlzaXMgZXhwbG9yYXRvcmlvIGNvbW8gdW4gYW7DoWxpc2lzIGNvbmZpcm1hdG9yaW8gcG9ycXVlIGVzIGZ1bmRhbWVudGFsbWVudGUgZW5nYcOxb3NvKi4KClNpIHJlYWxtZW50ZSBkZXNlYSByZWFsaXphciB1biBhbsOhbGlzaXMgY29uZmlybWF0b3JpbywgdW4gZW5mb3F1ZSBlcyBkaXZpZGlyIHN1cyBkYXRvcyBlbiB0cmVzIHBhcnRlcyBhbnRlcyBkZSBjb21lbnphciBlbCBhbsOhbGlzaXM6CgoxLiBFbCA2MCUgZGUgc3VzIGRhdG9zIHNlIGRlc3RpbmEgYSB1biBjb25qdW50byBkZSBlbnRyZW5hbWllbnRvLCAqdHJhaW5pbmcqLCAobyBleHBsb3JhY2nDs24pLiBQdWVkZSBoYWNlciBsbyBxdWUgcXVpZXJhIGNvbiBlc3RvcyBkYXRvczogdmlzdWFsaXphcmxvcyB5IGFqdXN0YXJsZXMgdG9uZWxhZGFzIGRlIG1vZGVsb3MuCgoyLiBFbCAyMCUgc2UgZGVzdGluYSBhIHVuIGNvbmp1bnRvIGRlIGNvbnN1bHRhcywgKnF1ZXJ5Ki4gUHVlZGUgdXNhciBlc3RvcyBkYXRvcyBwYXJhIGNvbXBhcmFyIG1vZGVsb3MgbyB2aXN1YWxpemFjaW9uZXMgYSBtYW5vLCBwZXJvIG5vIHB1ZWRlIHVzYXJsb3MgY29tbyBwYXJ0ZSBkZSB1biBwcm9jZXNvIGF1dG9tYXRpemFkby4KCjMuIEVsIDIwJSBzZSByZXRpZW5lIHBhcmEgdW5hIHBydWViYSAqdGVzdGluZyouIFNvbG8gcHVlZGUgdXNhciBlc3RvcyBkYXRvcyBVTkEgVkVaLCBwYXJhIHByb2JhciBzdSBtb2RlbG8gZmluYWwuCgpFc3RhIHBhcnRpY2nDs24gbGUgcGVybWl0ZSBleHBsb3JhciBsb3MgZGF0b3MgZGUgZW50cmVuYW1pZW50bywgZ2VuZXJhbmRvIG9jYXNpb25hbG1lbnRlIGhpcMOzdGVzaXMgY2FuZGlkYXRhcyBxdWUgdmVyaWZpY2EgY29uIGVsIGNvbmp1bnRvIGRlIGNvbnN1bHRhcy4gQ3VhbmRvIGVzdMOpIHNlZ3VybyBkZSBxdWUgdGllbmUgZWwgbW9kZWxvIGNvcnJlY3RvLCBwdWVkZSB2ZXJpZmljYXJsbyB1bmEgdmV6IGNvbiBsb3MgZGF0b3MgZGUgcHJ1ZWJhLgoKIyMgVGlkeW1vZGVscwoKCjxjZW50ZXI+CiFbXSguL2ltYWdlcy90aWR5bW9kZWxzLnBuZyl7d2lkdGg9MTglfQo8L2NlbnRlcj4KClRpZHltb2RlbHMsIGVzIHVuYSBpbnRlcmZheiBxdWUgdW5pZmljYSBiYWpvIHVuIMO6bmljbyBtYXJjbyBjaWVudG9zIGRlIGZ1bmNpb25lcyBkZSBkaXN0aW50b3MgcGFxdWV0ZXMsIGZhY2lsaXRhbmRvIGVuIGdyYW4gbWVkaWRhIHRvZGFzIGxhcyBldGFwYXMgZGUgcHJlcHJvY2VzYWRvLCBlbnRyZW5hbWllbnRvLCBvcHRpbWl6YWNpw7NuIHkgdmFsaWRhY2nDs24gZGUgbW9kZWxvcyBwcmVkaWN0aXZvcy4gTG9zIHBhcXVldGVzIHByaW5jaXBhbGVzIHF1ZSBmb3JtYW4gcGFydGUgZGVsIGVjb3Npc3RlbWEgdGlkeW1vZGVscyBzb246CgoqICoqcGFyc25pcDoqKiBwYXJhIGxhIGRlZmluaWNpw7NuIGRlIG1vZGVsb3MuIEltcGxlbWVudGFjacOzbiBgdGlkeWAgZGVsIGBjYXJldGAuCgoqICoqcmVjaXBlczoqKiBwYXJhIGVsIHByZXByb2Nlc2FkbyBkZSBkYXRvcyB5IGZlYXR1cmUgZW5naW5lZXJpbmcuCgoqICoqcnNhbXBsZToqKiBwYXJhIHZhbGlkYXIgbG9zIG1vZGVsb3MgcG9yIG3DqXRvZG9zIGRlIHJlc2FtcGxpbmcuCgoqICoqZGlhbHM6KiogcGFyYSBjcmVhciB5IG1hbmVqYXIgZWwgdmFsb3IgZGUgbG9zIGhpcGVycGFyw6FtZXRyb3MuCgoqICoqdHVuZToqKiBwYXJhIGhhY2VyIHR1bmluZyBkZSBtb2RlbG9zLgoKKiAqKnlhcmRzdGljazoqKiBwYXJhIGNhbGN1bGFyIG3DqXRyaWNhcyBkZSBtb2RlbG9zLgoKKiAqKndvcmtmbG93czoqKiBwYXJhIGNvbWJpbmFyIHRvZG9zIGxvcyBwYXNvcyBkZWwgcHJlcHJvY2VzYWRvIHkgbW9kZWxhZG8gZW4gdW4gw7puaWNvIG9iamV0by4KCjxjZW50ZXI+CiFbXSguL2ltYWdlcy90aWR5bW9kZWxzX3AucG5nKQo8L2NlbnRlcj4KCkVsIHByb2Nlc2FtaWVudG8gbG8gcmVhbGl6YW4gbG9zIHBhcXVldGVzIGByc2FtcGxlYCB5IGByZWNpcGVzYC4gRWwgbW9kZWxvIHByaW5jaXBhbCBlc3TDoSBlbiBgcGFyc25pcGAgKGVsIGVxdWl2YWxlbnRlIGRlIGBUaWR5YCBhIGBjYXJldGApIHkgcGFyYSBsYSB2YWxpZGFjacOzbiBgWWFyZHN0aWNrYC4gQ2FiZSBzZcOxYWxhciBxdWUgYGNhcmV0YCBzaWd1ZSB0ZW5pZW5kbyBsYSBtZWpvciBpbXBsZW1lbnRhY2nDs24gZGUgbGFzIHRhYmxhcyBkZSAqbWF0cmljZXMgZGUgY29uZnVzacOzbiosIGNvbiB1biBwYXF1ZXRlIGFkYXB0YWRvIGBDb25mdXNpb25UYWJsZVJgLgoKIyBEaXZpc2nDs24gVHJhaW4geSB0ZXN0CgpFdmFsdWFyIGxhIGNhcGFjaWRhZCBwcmVkaWN0aXZhIGRlIHVuIG1vZGVsbyBjb25zaXN0ZSBlbiBjb21wcm9iYXIgY8OzbW8gc2UgYWNlcmNhbiBsYXMgcHJlZGljY2lvbmVzIGEgbG9zIHZhbG9yZXMgdmVyZGFkZXJvcyBkZSBsYSB2YXJpYWJsZSByZXNwdWVzdGEuIFBhcmEgcG9kZXIgY3VhbnRpZmljYXJsbyBkZSBmb3JtYSBjb3JyZWN0YSwgc2UgbmVjZXNpdGEgZGlzcG9uZXIgZGUgdW4gY29uanVudG8gZGUgb2JzZXJ2YWNpb25lcywgZGUgbGFzIHF1ZSBzZSBjb25vemNhIGxhIHZhcmlhYmxlIHJlc3B1ZXN0YSwgcGVybyBxdWUgZWwgbW9kZWxvIG5vIGhheWEg4oCcdmlzdG/igJ0sIGVzIGRlY2lyLCBxdWUgbm8gaGF5YW4gcGFydGljaXBhZG8gZW4gc3UgYWp1c3RlLiBDb24gZXN0YSBmaW5hbGlkYWQsIHNlIGRpdmlkZSBlbCBjb25qdW50byBkZSBkYXRvcyBkaXNwb25pYmxlcyBlbiB1biBjb25qdW50byBkZSBlbnRyZW5hbWllbnRvICh0cmFpbikgeSB1biBjb25qdW50byBkZSBwcnVlYmEgKHRlc3QpLiAKCkVsIHRhbWHDsW8gYWRlY3VhZG8gZGUgbGFzIHBhcnRpY2lvbmVzIGRlcGVuZGUgZW4gZ3JhbiBtZWRpZGEgZGUgbGEgY2FudGlkYWQgZGUgZGF0b3MgZGlzcG9uaWJsZXMgeSBsYSBzZWd1cmlkYWQgcXVlIHNlIG5lY2VzaXRlIGVuIGxhIGVzdGltYWNpw7NuIGRlbCBlcnJvciwgODAlLTIwJSBzdWVsZSBkYXIgYnVlbm9zIHJlc3VsdGFkb3MuIEVsIHJlcGFydG8gZGViZSBoYWNlcnNlIGRlIGZvcm1hIGFsZWF0b3JpYSBvIGFsZWF0b3JpYS1lc3RyYXRpZmljYWRhLiBFdmFsdWFjacOzbiBkZSBtb2RlbG9zICh2ZXIgW2xpbmtdKGh0dHBzOi8vd3d3LmNpZW5jaWFkZWRhdG9zLm5ldC9kb2N1bWVudG9zLzMwX2Nyb3NzLXZhbGlkYXRpb25fb25lbGVhdmVvdXRfYm9vdHN0cmFwKSkuCgpQYXJhIGVzdGUgZWplbXBsbywgYW5hbGl6YXJlbW9zIGVsIGNvbmp1bnRvIGRlIGRhdG9zIGBTYXJhdG9nYUhpeXNlc2AsIHF1ZSBzZSBlbmN1ZW50cmEgZW4gZWwgcGFxdWV0ZSBgbmlzYXVjRGF0YWAsIGVsIGN1YWwgY29udGllbmUgaW5mb3JtYWNpw7NuIHNvYnJlIGxvcyBwcmVjaW8gZGUgMTcyOCB2aXZpZW5kYXMgZGUgU2FyYXRvZ2EgQ291bnR5LCBOZXcgWW9yaywgVVNBIGVuIGVsIGHDsW8gMjAwNi4gQWRlbcOhcyBkZWwgcHJlY2lvLCBlc3RlIGRhdGFzZXQgaW5jbHV5ZSBvdHJhcyAxNSB2YXJpYWJsZXM6CgoqIHByaWNlOiBwcmVjaW8gZGUgbGEgdml2aWVuZGEuCgoqIGxvdFNpemU6IG1ldHJvcyBjdWFkcmFkb3MgZGUgbGEgdml2aWVuZGEuCgoqIGFnZTogYW50aWfDvGVkYWQgZGUgbGEgdml2aWVuZGEuCgoqIGxhbmRWYWx1ZTogdmFsb3IgZGVsIHRlcnJlbm8uCgoqIGxpdmluZ0FyZWE6IG1ldHJvcyBjdWFkcmFkb3MgaGFiaXRhYmxlcy4KCiogcGN0Q29sbGVnZTogcG9yY2VudGFqZSBkZWwgdmVjaW5kYXJpbyBjb24gdMOtdHVsbyB1bml2ZXJzaXRhcmlvLgoKKiBiZWRyb29tczogbsO6bWVybyBkZSBkb3JtaXRvcmlvcy4KCiogZmlycGxhY2VzOiBuw7ptZXJvIGRlIGNoaW1lbmVhcy4KCiogYmF0aHJvb21zOiBuw7ptZXJvIGRlIGN1YXJ0b3MgZGUgYmHDsW8gKGVsIHZhbG9yIDAuNSBoYWNlIHJlZmVyZW5jaWEgYSBjdWFydG9zIGRlIGJhw7FvIHNpbiBkdWNoYSkuCgoqIHJvb21zOiBuw7ptZXJvIGRlIGhhYml0YWNpb25lcy4KCiogaGVhdGluZzogdGlwbyBkZSBjYWxlZmFjY2nDs24uCgoqIGZ1ZWw6IHRpcG8gZGUgYWxpbWVudGFjacOzbiBkZSBsYSBjYWxlZmFjY2nDs24gKGdhcywgZWxlY3RyaWNpZGFkIG8gZGllc2VsKS4KCiogc2V3ZXI6IHRpcG8gZGUgZGVzYWfDvGUuCgoqIHdhdGVyZnJvbnQ6IHNpIGxhIHZpdmllbmRhIHRpZW5lIHZpc3RhcyBhbCBsYWdvLgoKKiBuZXdDb25zdHJ1Y3Rpb246IHNpIGxhIHZpdmllbmRhIGVzIGRlIG51ZXZhIGNvbnN0cnVjY2nDs24uCgoqIGNlbnRyYWxBaXI6IHNpIGxhIHZpdmllbmRhIHRpZW5lIGFpcmUgYWNvbmRpY2lvbmFkby4KCkVsIG9iamV0aXZvIGVzIG9idGVuZXIgdW4gbW9kZWxvIGNhcGF6IGRlIHByZWRlY2lyIGVsIHByZWNpbyBkZWwgYWxxdWlsZXIgY2FzYS4gUG9yIGxvIHRhbnRvIHBhcmEgcG9kZXIgbGxhbWFyIGxvcyBkYXRvcyB0b21hbW9zIGVsIHNpZ3VpZW50ZSBjw7NkaWdvOgoKYGBge3J9CmRhdGEoIlNhcmF0b2dhSG91c2VzIiwgcGFja2FnZSA9ICJtb3NhaWNEYXRhIikKZGF0YSA8LSBTYXJhdG9nYUhvdXNlcwpjb2xuYW1lcyhkYXRhKQpgYGAKCkNvbiBlc3RlIGNvbmp1bnRvIGRlIGRhdG9zIHByb2NlZGVtb3MgYSBoYWNlciBsYSBkaXZpc2nDs24gZW50cmUgdHJhaW4geSB0ZXN0LgoKYGBge3J9CmxpYnJhcnkodGlkeW1vZGVscykKCiMgUmVwYXJ0byBkZSBkYXRvcyBlbiB0cmFpbiB5IHRlc3QKc2V0LnNlZWQoMTIzKQpzcGxpdF9pbmljaWFsIDwtIGluaXRpYWxfc3BsaXQoCiAgICAgICAgICAgICAgICAgICAgZGF0YSAgID0gZGF0YSwgIyBkYXRvcyBxdWUgcXVlcmVtb3MgZGl2aWRpcgogICAgICAgICAgICAgICAgICAgIHByb3AgICA9IDAuOCwgIyBQcm9wb3JjacOzbiBkZWwgODAlCiAgICAgICAgICAgICAgICAgICAgc3RyYXRhID0gcHJpY2UgIyBWYXJpYWJsZSByZXNwdWVzdGEgCiAgICAgICAgICAgICAgICAgKQpkYXRhX3RyYWluIDwtIHRyYWluaW5nKHNwbGl0X2luaWNpYWwpCmRhdGFfdGVzdCAgPC0gdGVzdGluZyhzcGxpdF9pbmljaWFsKQpgYGAKCkVzIGltcG9ydGFudGUgdmVyaWZpY2FyIHF1ZSBsYSBkaXN0cmlidWNpw7NuIGRlIGxhIHZhcmlhYmxlIHJlc3B1ZXN0YSBlcyBzaW1pbGFyIGVuIGVsIGNvbmp1bnRvIGRlIGVudHJlbmFtaWVudG8geSBlbiBlbCBkZSB0ZXN0LiBQYXJhIGFzZWd1cmFyIHF1ZSBlc3RvIHNlIGN1bXBsZSwgbGEgZnVuY2nDs24gYGluaXRpYWxfc3BsaXQoKWAgcGVybWl0ZSBpZGVudGlmaWNhciBjb24gZWwgYXJndW1lbnRvIGBzdHJhdGFgIGxhIHZhcmlhYmxlIGVuIGJhc2UgYSBsYSBjdWFsIGhhY2VyIGVsIHJlcGFydG8uCgpgYGB7cn0Kc3VtbWFyeShkYXRhX3RyYWluJHByaWNlKQpgYGAKCmBgYHtyfQpzdW1tYXJ5KGRhdGFfdGVzdCRwcmljZSkKYGBgCgpFc3RlIHRpcG8gZGUgcmVwYXJ0byBlc3RyYXRpZmljYWRvIGFzZWd1cmEgcXVlIGVsIGNvbmp1bnRvIGRlIGVudHJlbmFtaWVudG8geSBlbCBkZSB0ZXN0IHNlYW4gc2ltaWxhcmVzIGVuIGN1YW50byBhIGxhIHZhcmlhYmxlIHJlc3B1ZXN0YSwgc2luIGVtYmFyZ28sIG5vIGdhcmFudGl6YSBxdWUgb2N1cnJhIGxvIG1pc21vIGNvbiBsb3MgcHJlZGljdG9yZXMuIFBvciBlamVtcGxvLCBlbiB1biBzZXQgZGUgZGF0b3MgY29uIDEwMCBvYnNlcnZhY2lvbmVzLCB1biBwcmVkaWN0b3IgYmluYXJpbyBxdWUgdGVuZ2EgOTAgb2JzZXJ2YWNpb25lcyBkZSB1biBncnVwbyB5IHNvbG8gMTAgZGUgb3RybywgdGllbmUgdW4gYWx0byByaWVzZ28gZGUgcXVlLCBlbiBhbGd1bmEgZGUgbGFzIHBhcnRpY2lvbmVzLCBlbCBncnVwbyBtaW5vcml0YXJpbyBubyB0ZW5nYSByZXByZXNlbnRhbnRlcy4gU2kgZXN0byBvY3VycmUgZW4gZWwgY29uanVudG8gZGUgZW50cmVuYW1pZW50bywgYWxndW5vcyBhbGdvcml0bW9zIGRhcsOhbiBlcnJvciBhbCBhcGxpY2FybG9zIGFsIGNvbmp1bnRvIGRlIHRlc3QsIHlhIHF1ZSBubyBlbnRlbmRlcsOhbiBlbCB2YWxvciBxdWUgc2UgbGVzIGVzdMOhIHBhc2FuZG8uIEVzdGUgcHJvYmxlbWEgcHVlZGUgZXZpdGFyc2UgZWxpbWluYW5kbyB2YXJpYWJsZXMgY29uIHZhcmlhbnphIHByw7N4aW1hIGEgY2VybywgbG8gY3VhbCB2ZXJlbW9zIG3DoXMgYWRlbGFudGUuCgojIyBFeGNsdXNpw7NuIGRlIHZhcmlhYmxlcyBjb24gdmFyaWFuemEgcHLDs3hpbWEgYSBjZXJvCgpObyBzZSBkZWJlbiBpbmNsdWlyIGVuIGVsIG1vZGVsbyBwcmVkaWN0b3JlcyBxdWUgY29udGVuZ2FuIHVuIMO6bmljbyB2YWxvciAodmFyaWFuemEtY2VybykgeWEgcXVlIG5vIGFwb3J0YW4gaW5mb3JtYWNpw7NuLiBUYW1wb2NvIGVzIGNvbnZlbmllbnRlIGluY2x1aXIgcHJlZGljdG9yZXMgcXVlIHRlbmdhbiB1bmEgdmFyaWFuemEgcHLDs3hpbWEgYSBjZXJvLCBlcyBkZWNpciwgcHJlZGljdG9yZXMgcXVlIHRvbWFuIHNvbG8gdW5vcyBwb2NvcyB2YWxvcmVzLCBkZSBsb3MgY3VhbGVzLCBhbGd1bm9zIGFwYXJlY2VuIGNvbiBtdXkgcG9jYSBmcmVjdWVuY2lhLiBFbCBwcm9ibGVtYSBjb24gZXN0b3Mgw7psdGltb3MgZXMgcXVlIHB1ZWRlbiBjb252ZXJ0aXJzZSBlbiBwcmVkaWN0b3JlcyBjb24gdmFyaWFuemEgY2VybyBjdWFuZG8gc2UgZGl2aWRlbiBsYXMgb2JzZXJ2YWNpb25lcyBwb3IgdmFsaWRhY2nDs24gY3J1emFkYSBvIGBib290c3RyYXAuYAoKwr9SZWN1ZXJkYXMgcXVlIGxvIHZpbW9zIGVuIGVsIGFuw6FsaXNpcyBleHBsb3JhdG9yaW8sIEVEQT8KCkFxdcOtLCB0YW1iacOpbiBwb2RlbW9zIHV0aWxpemFyIGxhIGZ1bmNpw7NuIGBzdGVwX256digpYCBkZWwgcGFxdWV0ZSBgcmVjaXBlc2AgcXVlIGlkZW50aWZpY2EgcHJlZGljdG9yZXMgcG90ZW5jaWFsbWVudGUgcHJvYmxlbcOhdGljb3MsIGVzIGRlY2lyLCBhcXVlbGxvcyBxdWUgdGllbmUgdW4gw7puaWNvIHZhbG9yIG8gdmFyaWFuemEtIGNlcm8gcXVlIGN1bXBsZW4gZG9zIGNvbmRpY2lvbmVzOgoKKiBWYXJpYWNpw7NuIGRlIGZyZWN1ZW5jaWE6ICBFcyBsYSB2YXJpYWNpw7NuIGVudHJlIGxhIGZyZWN1ZW5jaWEgZGVsIHZhbG9yIG3DoXMgY29tw7puIHkgbGEgZnJlY3VlbmNpYSBkZWwgc2Vnw7puIHZhbG9yIG3DoXMgY29tw7puLiAgRXN0ZSByYXRpbyB0aWVuZGUgYSAxIHNpIGxhcyBmcmVjdWVuY2lhcyBlc3TDoW4gZXF1aWRpc3RyaWJ1aWRhcyB5IGEgdmFsb3JlcyBncmFuZGVzIGN1YW5kbyBsYSByZWZ1ZW5jaWEgZGVsIHZhbG9yIG1heW9yaXRhcmlvIHN1cGVyYSBwb3IgbXVjaG8gYWwgcmVzdG8gKGVsIGRlbm9taW5hZG9yIGVzIHVuIG7Dum1lcm8gZGVjaW1hbCBwZXF1ZcOxbykuIFZhbG9yIHBvciBkZWZlY3RvIGBmcmVxX2N1dCA9IDk1LzVgLgoKKiBQb3JjZW50YWplIGRlIHZhbG9yZXMgw7puaWNvczogbsO6bWVybyBkZSB2YWxvcmVzIMO6bmljb3MgZGl2aWRvIGVudHJlIGVsIHRvdGFsIGRlIGxhIG11ZXN0cmEgKG11bHRpcGxpY2FkbyBwb3IgMTAwKS4gIEVzdGEgdmFsb3Igc2UgYWNlcmNhIGEgY2VybyBjdWFuZG8gbGEgdmFyaWFuemEtY2VybyBlcyBtYXlvci4gVmFsb3IgcG9yIGRlZmVjdG8gYHVuaXF1ZUN1dCA9IDEwYC4gCgpTaSBiaWVuLCBjdWFuZG8gZWxpbWluYW1vcyBwcmVkaWN0b3JlcyBubyBpbmZvcm1hdGl2b3MgcG9kcsOtYSBjb25zaWRlcmFyc2UgdW4gcGFzbyBwcm9waW8gZGVsIHByb2Nlc28gZGUgc2VsZWNjacOzbiBkZSBwcmVkaWN0b3JlcywgZGFkbyBxdWUgY29uc2lzdGUgZW4gdW4gZmlsdHJhZG8gcG9yIHZhcmlhbnphLCB0aWVuZSBxdWUgcmVhbGl6YXJzZSBhbnRlcyBkZSBlc3RhbmRhcml6YXIgbG9zIGRhdG9zLCB5YSBxdWUgZGVzcHXDqXMsIHRvZG9zIGxvcyBwcmVkaWN0b3JlcyB0aWVuZW4gdmFyaWFuemEgMS4KCiMjIEVzdGFuZGFyaXphY2nDs24geSBlc2NhbGFkbyBkZSB2YXJpYWJsZXMgbnVtw6lyaWNhcwoKQ3VhbmRvIGxvcyBwcmVkaWN0b3JlcyBzb24gbnVtw6lyaWNvcywgbGEgZXNjYWxhIGVuIGxhIHF1ZSBzZSBtaWRlbiwgYXPDrSBjb21vIGxhIG1hZ25pdHVkIGRlIHN1IHZhcmlhbnphIHB1ZWRlbiBpbmZsdWlyIGVuIGdyYW4gbWVkaWRhIGVuIGVsIG1vZGVsby4gTXVjaG9zIGFsZ29yaXRtb3MgZGUgKm1hY2hpbmUgbGVhcm5pbmcqIChTVk0sIHJlZGVzIG5ldXJvbmFsZXMsIGxhc3NvLCBldGMpIHNvbiBzZW5zaWJsZXMgYSBlc3RvLCBkZSBmb3JtYSBxdWUsIHNpIG5vIHNlIGlndWFsYW4gZGUgYWxndW5hIGZvcm1hIGxvcyBwcmVkaWN0b3JlcywgYXF1ZWxsb3MgcXVlIHNlIG1pZGFuIGVuIHVuYSBlc2NhbGEgbWF5b3IgbyBxdWUgdGVuZ2FuIG3DoXMgdmFyaWFuemEgZG9taW5hcsOhbiBlbCBtb2RlbG8gYXVucXVlIG5vIHNlYW4gbG9zIHF1ZSBtw6FzIHJlbGFjacOzbiB0aWVuZW4gY29uIGxhIHZhcmlhYmxlIHJlc3B1ZXN0YS4gRXhpc3RlbiBwcmluY2lwYWxtZW50ZSAyIGVzdHJhdGVnaWFzIHBhcmEgZXZpdGFybG86CgoqICoqQ2VudHJhZG86KiogY29uc2lzdGUgZW4gcmVzdGFybGUgYSBjYWRhIHZhbG9yIGxhIG1lZGlhIGRlbCBwcmVkaWN0b3IgYWwgcXVlIHBlcnRlbmVjZS4gU2kgbG9zIGRhdG9zIGVzdMOhbiBhbG1hY2VuYWRvcyBlbiB1biBkYXRhZnJhbWUsIGVsIGNlbnRyYWRvIHNlIGNvbnNpZ3VlIHJlc3TDoW5kb2xlIGEgY2FkYSB2YWxvciBsYSBtZWRpYSBkZSBsYSBjb2x1bW5hIGVuIGxhIHF1ZSBzZSBlbmN1ZW50cmEuIENvbW8gcmVzdWx0YWRvIGRlIGVzdGEgdHJhbnNmb3JtYWNpw7NuLCB0b2RvcyBsb3MgcHJlZGljdG9yZXMgcGFzYW4gYSB0ZW5lciB1bmEgbWVkaWEgZGUgY2VybywgZXMgZGVjaXIsIGxvcyB2YWxvcmVzIHNlIGNlbnRyYW4gZW4gdG9ybm8gYWwgb3JpZ2VuLgoKKiAqKk5vcm1hbGl6YWNpw7NuIChlc3RhbmRhcml6YWNpw7NuKToqKiBjb25zaXN0ZSBlbiB0cmFuc2Zvcm1hciBsb3MgZGF0b3MgZGUgZm9ybWEgcXVlIHRvZG9zIGxvcyBwcmVkaWN0b3JlcyBlc3TDqW4gYXByb3hpbWFkYW1lbnRlIGVuIGxhIG1pc21hIGVzY2FsYS4gSGF5IGRvcyBmb3JtYXMgZGUgbG9ncmFybG86CgogICsgKk5vcm1hbGl6YWNpw7NuIFotc2NvcmU6KiBkaXZpZGlyIGNhZGEgcHJlZGljdG9yIGVudHJlIHN1IGRlc3ZpYWNpw7NuIHTDrXBpY2EgZGVzcHXDqXMgZGUgaGFiZXIgc2lkbyBjZW50cmFkbywgZGUgZXN0YSBmb3JtYSwgbG9zIGRhdG9zIHBhc2FuIGEgdGVuZXIgdW5hIGRpc3RyaWJ1Y2nDs24gbm9ybWFsLgogIAogICQkCiAgeiA9IFxmcmFje3ggLSBcbXV9e1xzaWdtYX0KICAkJAogIAogICsgKkVzdGFuZGFyaXphY2nDs24gbWF4LW1pbjoqIHRyYW5zZm9ybWFyIGxvcyBkYXRvcyBkZSBmb3JtYSBxdWUgZXN0w6luIGRlbnRybyBkZWwgcmFuZ28gWzAsIDFdLgoKJCQKeF97bm9tfT1cZnJhY3t4LXhfe21pbn19e3hfe21heH0teF97bWlufX0KJCQKKipOT1RBOioqIE51bmNhIHNlIGRlYmUgZXN0YW5kYXJpemFyIGxhcyB2YXJpYWJsZXMgZGVzcHXDqXMgZGUgc2VyIGJpbmFyaXphZGFzLgoKCiMjIEJpbmFyaXphY2nDs24gZGUgbGFzIHZhcmlhYmxlcyBjdWFsaXRhdGl2YXMKCkxhIGJpbmFyaXphY2nDs24gY29uc2lzdGUgZW4gY3JlYXIgbnVldmFzIHZhcmlhYmxlcyAqZHVtbXkqIGNvbiBjYWRhIHVubyBkZSBsb3Mgbml2ZWxlcyBkZSBsYXMgdmFyaWFibGVzIGN1YWxpdGF0aXZhcy4gQSBlc3RlIHByb2Nlc28gdGFtYmnDqW4gc2UgbGUgY29ub2NlIGNvbW8gKm9uZSBob3QgZW5jb2RpbmcqLiBQb3IgZWplbXBsbywgdW5hIHZhcmlhYmxlIGxsYW1hZGEgY29sb3IgcXVlIGNvbnRlbmdhIGxvcyBuaXZlbGVzIHJvam8sIHZlcmRlIHkgYXp1bCwgc2UgY29udmVydGlyw6EgZW4gdHJlcyBudWV2YXMgdmFyaWFibGVzIChgY29sb3Jfcm9qb2AsIGBjb2xvcl92ZXJkZWAsIGBjb2xvcl9henVsYCksIHRvZGFzIGNvbiBlbCB2YWxvciAwIGV4Y2VwdG8gbGEgcXVlIGNvaW5jaWRlIGNvbiBsYSBvYnNlcnZhY2nDs24sIHF1ZSB0b21hIGVsIHZhbG9yIDEuCgpQb3IgZGVmZWN0bywgbGEgZnVuY2nDs24gYHN0ZXBfZHVtbXkoYWxsX25vbWluYWwoKSwgLWFsbF9vdXRjb21lcygpKWAgYmluYXJpemEgdG9kYXMgbGFzIHZhcmlhYmxlcyBhbG1hY2VuYWRhcyBjb21vIHRpcG8gYGZhY3RvcmAgbyBgY2hhcmFjdGVyYCwgZXhjZXB0byBsYSB2YXJpYWJsZSByZXNwdWVzdGEuIEFkZW3DoXMsIGVsaW1pbmEgdW5vIGRlIGxvcyBuaXZlbGVzIHBhcmEgZXZpdGFyIHJlZHVuZGFuY2lhcy4gVm9sdmllbmRvIGFsIGVqZW1wbG8gYW50ZXJpb3IsIG5vIGVzIG5lY2VzYXJpbyBhbG1hY2VuYXIgbGFzIHRyZXMgdmFyaWFibGVzLCB5YSBxdWUsIHNpIGNvbG9yX3Jvam8geSBjb2xvcl92ZXJkZSB0b21hbiBlbCB2YWxvciAwLCBsYSB2YXJpYWJsZSBjb2xvcl9henVsIHRvbWEgbmVjZXNhcmlhbWVudGUgZWwgdmFsb3IgMS4gU2kgYGNvbG9yX3Jvam9gIG8gYGNvbG9yX3ZlcmRlYCB0b21hbiBlbCB2YWxvciAxLCBlbnRvbmNlcyBjb2xvcl9henVsIGVzIG5lY2VzYXJpYW1lbnRlIDAuCgpFbCBwYXF1ZXRlIGByZWNpcGVzYCBpbmNvcnBvcmEgdW5hIGFtcGxpYSB2YXJpZWRhZCBkZSBmdW5jaW9uZXMgcGFyYSBwcmVwcm9jZXNhciBsb3MgZGF0b3MsIGZhY2lsaXRhbmRvIGVsIGFwcmVuZGl6YWplIGRlIGxhcyB0cmFuc2Zvcm1hY2lvbmVzIMO6bmljYW1lbnRlIGNvbiBvYnNlcnZhY2lvbmVzIGRlIGVudHJlbmFtaWVudG8sIHkgcG9kZXIgYXBsaWNhcmxhcyBkZXNwdcOpcyBhIGN1YWxxdWllciBjb25qdW50byBkZSBkYXRvcy4gTGEgaWRlYSBkZXRyw6FzIGRlIGVzdGUgcGFxdWV0ZSBlcyBsYSBzaWd1aWVudGU6CgoxLiBEZWZpbmlyIGN1w6FsIGVzIGxhIHZhcmlhYmxlIHJlc3B1ZXN0YSwgbG9zIHByZWRpY3RvcmVzIHkgZWwgY29uanVudG8gZGUgZGF0b3MgZGUgZW50cmVuYW1pZW50bywgYHJlY2lwZSgpYC4KCjIuIERlZmluaXIgdG9kYXMgbGFzIHRyYW5zZm9ybWFjaW9uZXMgKGVzY2FsYWRvLCBzZWxlY2Npw7NuLCBmaWx0cmFkb+KApikgcXVlIHNlIGRlc2VhIGFwbGljYXIsIGBzdGVwXygpYC4KCjMuIEFwcmVuZGVyIGxvcyBwYXLDoW1ldHJvcyBuZWNlc2FyaW9zIHBhcmEgZGljaGFzIHRyYW5zZm9ybWFjaW9uZXMgY29uIGxhcyBvYnNlcnZhY2lvbmVzIGRlIGVudHJlbmFtaWVudG8gYHJlcCgpYC4KCjQuIEFwbGljYXIgbGFzIHRyYW5zZm9ybWFjaW9uZXMgYXByZW5kaWRhcyBhIGN1YWxxdWllciBjb25qdW50byBkZSBkYXRvcyBganVpY2UoKWAsIGBiYWtlKClgLgoKYGBge3J9CiMgU2UgYWxtYWNlbmFuIGVuIHVuIG9iamV0byBgcmVjaXBlYCB0b2RvcyBsb3MgcGFzb3MgZGUgcHJlcHJvY2VzYWRvIHksIGZpbmFsbWVudGUsIHNlIGFwbGljYW4gYSBsb3MgZGF0b3MuCnRyYW5zZm9ybWVyIDwtIHJlY2lwZSgKICAgICAgICAgICAgICAgICAgZm9ybXVsYSA9IHByaWNlIH4gLiwKICAgICAgICAgICAgICAgICAgZGF0YSA9ICBkYXRhX3RyYWluCiAgICAgICAgICAgICAgICkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfbmFvbWl0KGFsbF9wcmVkaWN0b3JzKCkpICU+JQogICAgICAgICAgICAgICBzdGVwX256dihhbGxfcHJlZGljdG9ycygpKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9jZW50ZXIoYWxsX251bWVyaWMoKSwgLWFsbF9vdXRjb21lcygpKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9zY2FsZShhbGxfbnVtZXJpYygpLCAtYWxsX291dGNvbWVzKCkpICU+JQogICAgICAgICAgICAgICBzdGVwX2R1bW15KGFsbF9ub21pbmFsKCksIC1hbGxfb3V0Y29tZXMoKSkKCnRyYW5zZm9ybWVyCmBgYApVbmEgdmV6IHF1ZSBzZSBoYSBkZWZpbmlkbyBlbCBvYmpldG8gcmVjaXBlLCBjb24gbGEgZnVuY2nDs24gYHByZXAoKWAgc2UgYXByZW5kZW4gbGFzIHRyYW5zZm9ybWFjaW9uZXMgY29uIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvIHkgc2UgYXBsaWNhbiBhIGxvcyBkb3MgY29uanVudG9zIGNvbiBgYmFrZSgpYC4KCmBgYHtyfQojIFNlIGVudHJlbmEgZWwgb2JqZXRvIHJlY2lwZQp0cmFuc2Zvcm1lcl9maXQgPC0gcHJlcCh0cmFuc2Zvcm1lcikKCiMgU2UgYXBsaWNhbiBsYXMgdHJhbnNmb3JtYWNpb25lcyBhbCBjb25qdW50byBkZSBlbnRyZW5hbWllbnRvIHkgZGUgdGVzdApkYXRhX3RyYWluX3ByZXAgPC0gYmFrZSh0cmFuc2Zvcm1lcl9maXQsIG5ld19kYXRhID0gZGF0YV90cmFpbikKZGF0YV90ZXN0X3ByZXAgIDwtIGJha2UodHJhbnNmb3JtZXJfZml0LCBuZXdfZGF0YSA9IGRhdGFfdGVzdCkKCmdsaW1wc2UoZGF0YV90cmFpbl9wcmVwKQpgYGAKVHJhcyBlbCBwcmVwcm9jZXNhZG8gZGUgbG9zIGRhdG9zLCBzZSBoYW4gZ2VuZXJhZG8gdW4gdG90YWwgZGUgMTggdmFyaWFibGVzICgxNyBwcmVkaWN0b3JlcyB5IGxhIHZhcmlhYmxlIHJlc3B1ZXN0YSkuCgojIE1vZGVsYWRvCgpZYSBhbmFsaXphZG8gbG9zIGRhdG9zIChlbnRlbmRlcmxvcyBwb3IgbWVkaW8gZGVsIEVEQSksIHF1ZSBoYW4gc2lkbyBwcmVwcm9jZXNhZG9zIHkgbG9zIHByZWRpY3RvcmVzIHNlbGVjY2lvbmFkb3MsIGVsIHNpZ3VpZW50ZSBwYXNvIGVzIGVtcGxlYXIgdW4gYWxnb3JpdG1vIGRlICptYWNoaW5lIGxlYXJuaW5nKiBxdWUgcGVybWl0YSBjcmVhciB1biBtb2RlbG8gY2FwYXogZGUgcmVwcmVzZW50YXIgbG9zIHBhdHJvbmVzIHByZXNlbnRlcyBlbiBsb3MgZGF0b3MgZGUgZW50cmVuYW1pZW50byB5IGdlbmVyYWxpemFybG9zIGEgbnVldmFzIG9ic2VydmFjaW9uZXMuIEVuY29udHJhciBlbCBtZWpvciBtb2RlbG8gbm8gZXMgZsOhY2lsLCBleGlzdGVuIG11bHRpdHVkIGRlIGFsZ29yaXRtb3MsIGNhZGEgdW5vIGNvbiB1bmFzIGNhcmFjdGVyw61zdGljYXMgcHJvcGlhcyB5IGNvbiBkaXN0aW50b3MgcGFyw6FtZXRyb3MgcXVlIGRlYmVuIHNlciBhanVzdGFkb3MuIFBvciBsbyBnZW5lcmFsLCBsYXMgZXRhcGFzIHNlZ3VpZGFzIHBhcmEgb2J0ZW5lciB1biBidWVuIG1vZGVsbyBzb246CgoqICoqQWp1c3RlL2VudHJlbmFtaWVudG86KiogY29uc2lzdGUgZW4gYXBsaWNhciB1biBhbGdvcml0bW8gZGUgbWFjaGluZSBsZWFybmluZyBhIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvIHBhcmEgcXVlIGVsIG1vZGVsbyBhcHJlbmRhLgoKKiAqKkV2YWx1YWNpw7NuL3ZhbGlkYWNpw7NuOioqIGVsIG9iamV0aXZvIGRlIHVuIG1vZGVsbyBwcmVkaWN0aXZvIG5vIGVzIHNlciBjYXBheiBkZSBwcmVkZWNpciBvYnNlcnZhY2lvbmVzIHF1ZSB5YSBzZSBjb25vY2VuLCBzaW5vIG51ZXZhcyBvYnNlcnZhY2lvbmVzIHF1ZSBlbCBtb2RlbG8gbm8gaGEgdmlzdG8uIFBhcmEgcG9kZXIgZXN0aW1hciBlbCBlcnJvciBxdWUgY29tZXRlIHVuIG1vZGVsbyBlcyBuZWNlc2FyaW8gcmVjdXJyaXIgYSBlc3RyYXRlZ2lhcyBkZSB2YWxpZGFjacOzbiBlbnRyZSBsYXMgcXVlIGRlc3RhY2FuOiB2YWxpZGFjacOzbiBzaW1wbGUsIGBib290c3RyYXBgIHkgdmFsaWRhY2nDs24gY3J1emFkYS4KCiogKipPcHRpbWl6YWNpw7NuIGRlIGhpcGVycGFyw6FtZXRyb3M6KiogbXVjaG9zIGFsZ29yaXRtb3MgZGUgbWFjaGluZSBsZWFybmluZyBjb250aWVuZW4gZW4gc3VzIGVjdWFjaW9uZXMgdW5vIG8gdmFyaW9zIHBhcsOhbWV0cm9zIHF1ZSBubyBzZSBhcHJlbmRlbiBjb24gbG9zIGRhdG9zLCBhIGVzdG9zIHNlIGxlcyBjb25vY2UgY29tbyBoaXBlcnBhcsOhbWV0cm9zLiBQb3IgZWplbXBsbywgbG9zIFtTVk0gbGluZWFsXShodHRwczovL3d3dy5jaWVuY2lhZGVkYXRvcy5uZXQvZG9jdW1lbnRvcy8zNF9tYXF1aW5hc19kZV92ZWN0b3Jfc29wb3J0ZV9zdXBwb3J0X3ZlY3Rvcl9tYWNoaW5lcykgdGllbmUgZWwgaGlwZXJwYXLDoW1ldHJvIGRlIGNvc3RlICpDKiB5IGxvcyBbw6FyYm9sZXMgZGUgcmVncmVzacOzbl0oaHR0cHM6Ly93d3cuY2llbmNpYWRlZGF0b3MubmV0L2RvY3VtZW50b3MvMzNfYXJib2xlc19kZV9wcmVkaWNjaW9uX2JhZ2dpbmdfcmFuZG9tX2ZvcmVzdF9ib29zdGluZykgbGEgcHJvZnVuZGlkYWQgZGVsIMOhcmJvbCBgdHJlZV9kZXB0aGAgeSBlbCBuw7ptZXJvIG3DrW5pbW8gZGUgb2JzZXJ2YWNpb25lcyBwb3Igbm9kbyBgbWluX25gLiBObyBleGlzdGUgZm9ybWEgZGUgY29ub2NlciBkZSBhbnRlbWFubyBjdcOhbCBlcyBlbCB2YWxvciBleGFjdG8gZGUgdW4gaGlwZXJwYXLDoW1ldHJvIHF1ZSBkYSBsdWdhciBhbCBtZWpvciBtb2RlbG8sIHBvciBsbyBxdWUgc2UgdGllbmUgcXVlIHJlY3VycmlyIGEgZXN0cmF0ZWdpYXMgZGUgdmFsaWRhY2nDs24gcGFyYSBjb21wYXJhciBkaXN0aW50b3MgdmFsb3Jlcy4KCiogKipQcmVkaWNjacOzbjoqKiB1bmEgdmV6IGNyZWFkbyBlbCBtb2RlbG8sIGVzdGUgc2UgZW1wbGVhIHBhcmEgcHJlZGVjaXIgbnVldmFzIG9ic2VydmFjaW9uZXMuCgpFcyBhIGxvIGxhcmdvIGRlIHRvZG8gZXN0ZSBwcm9jZXNvIGRvbmRlIG3DoXMgZGVzdGFjYW4gbGFzIGZ1bmNpb25hbGlkYWRlcyBvZnJlY2lkYXMgcG9yIGB0aWR5bW9kZWxzYCwgcGVybWl0aWVuZG8gZW1wbGVhciBsYSBtaXNtYSBzaW50YXhpcyBwYXJhIGFqdXN0YXIsIG9wdGltaXphciwgZXZhbHVhciB5IHByZWRlY2lyIHVuIGFtcGxpbyBhYmFuaWNvIGRlIG1vZGVsb3MsIHZhcmlhbmRvIMO6bmljYW1lbnRlIGVsIG5vbWJyZSBkZWwgYWxnb3JpdG1vLiAKCkF1bnF1ZSBgdGlkeW1vZGVsc2AgcGVybWl0ZSB0b2RvIGVzdG8gY29uIGFwZW5hcyB1bmFzIHBvY2FzIGzDrW5lYXMgZGUgY8OzZGlnbywgc29uIG11Y2hvcyBsb3MgYXJndW1lbnRvcyBxdWUgcHVlZGVuIHNlciBhZGFwdGFkb3MsIGNhZGEgdW5vIGNvbiBtw7psdGlwbGVzIHBvc2liaWxpZGFkZXMuIENvbiBlbCBvYmpldGl2byBkZSBleHBvbmVyIG1lam9yIGNhZGEgdW5hIGRlIGxhcyBvcGNpb25lcywgZW4gbHVnYXIgZGUgY3JlYXIgZGlyZWN0YW1lbnRlIHVuIG1vZGVsbyBmaW5hbCwgc2UgbXVlc3RyYW4gZWplbXBsb3MgZGUgbWVub3IgYSBtYXlvciBjb21wbGVqaWRhZC4KCiMjIEVudHJlbmFtaWVudG8KCkxvcyBtb2RlbG9zIGRlIGB0aWR5bW9kZWxzYCBzb24gYWNjZXNpYmxlcyBwb3IgbWVkaW8gZGVsIHBhcXVldGUgYHBhcnNuaXBgLiBBdW5xdWUgdW4gbWlzbW8gbW9kZWxvIHB1ZWRlIGVzdGFyIGVuIHZhcmlvcyBwYXF1ZXRlcyBjb24gbm9tYnJlcyBkaXN0aW50b3MuIAoKPGNlbnRlcj4KIVtdKC4vaW1hZ2VzL3BhcnNuaXAucG5nKXt3aWR0aD0xNSV9CjwvY2VudGVyPgoKUGFyYSBjcmVhciB1biBtb2RlbG8gc2UgcmVxdWllcmVuIMO6bmljYW1lbnRlIHRyZXMgcGFzb3M6CgoqIERlZmluaXIgZWwgdGlwbyBkZSBtb2RlbG8geSBzdXMgcGFyw6FtZXRyb3MKCiogRGVmaW5pciBxdcOpIGltcGxlbWVudGFjacOzbiBkZWwgbW9kZWxvIHNlIHF1aWVyZSBhcGxpY2FyIChgZW5naW5lYCkKCiogQWp1c3RhciBlbCBtb2RlbG8KClZhbW9zIGEgYWp1c3RhciB1biBtb2RlbG8gZGUgW8OhcmJvbCBkZSByZWdyZXNpw7NuXShodHRwczovL3d3dy5jaWVuY2lhZGVkYXRvcy5uZXQvZG9jdW1lbnRvcy8zM19hcmJvbGVzX2RlX3ByZWRpY2Npb25fYmFnZ2luZ19yYW5kb21fZm9yZXN0X2Jvb3N0aW5nKSBwYXJhIHByZWRlY2lyIGVsIHByZWNpbyBkZSBsYSB2aXZpZW5kYSBkZSBTYXJhdG9nYSBlbiBmdW5jacOzbiBkZSB0b2RvcyBsb3MgcHJlZGljdG9yZXMgZGlzcG9uaWJsZXMuICAgCkEgZXhjZXBjacOzbiBkZSBsYSBpbXBsZW1lbnRhY2nDs24gZGVsIGFsZ29yaXRtbyAoYCJycGFydCJgKSwgYHJwYXJ0YCAodmllbmUgcG9yIGRlZmF1bHQpIG8gYCJDNS4wImAgKHNvbG8gcGFyYSBjbGFzaWZpY2FjacOzbiksIHRvZG9zIGxvcyBkZW3DoXMgYXJndW1lbnRvcyBkZSBsYSBmdW5jacOzbiBgZGVjaXNpb25fdHJlZSgpYCBzZSBkZWphbiBwb3IgZGVmZWN0by4KCmBgYHtyfQptb2RlbF90cmVlIDwtIGRlY2lzaW9uX3RyZWUobW9kZSA9ICJyZWdyZXNzaW9uIikgJT4lCiAgICAgICAgICAgICAgIHNldF9lbmdpbmUoZW5naW5lID0gInJwYXJ0IikKbW9kZWxfdHJlZQpgYGAKCkNvbiBsYXMgbMOtbmVhcyBhbnRlcmlvcmVzIHPDs2xvIGd1YXJkYW1vcyBlbCB0aXBvIGRlIGFsZ29yaXRtbyAoc3VzIHBhcsOhbWV0cm9zLCBoaXBlcnBhcsOhbWV0cm9zIHkgZWwgcGFxdWV0ZSBlbiBlbCBxdWUgZXN0w6EgaW1wbGVtZW50YWRvKS4gRWwgc2lnaWVudGUgcGFzbyBlcyBhanVzdGFyIGVsIG1vZGVsbywgcGFyYSBsb3MgY3VhbCB0ZW5lbW9zIGRvcyBvcGNpb25lczoKCiogYGZpdCgpYCBxdWUgZW1wbGVhIGNvbW8gYXJndW1lbnRvIHVuYSBmb3JtdWxhIHBhcmEgZGVmaW5pciBsYSB2YXJpYWJsZSByZXNwdWVzdGEgeSBsb3MgcHJlZGljdG9yZXMuCgoqIGBmaXRfeHkoKWAgcXVlIGVtcGxlYSBsb3MgYXJndW1lbnRvcyBgeGAgeSBgeWAgcGFyYSBkZWZpbmlyIGxhIG1hdHJpeiBkZSBwcmVkaWN0b3JlcyB5IGVsIHZlY3RvciBjb24gbGEgdmFyaWFibGUgcmVzcHVlc3RhLgoKYGBge3J9CiMgRW50cmVuYW1pZW50byBlbXBsZWFuZG8gZsOzcm11bGEKbW9kZWxfdHJlZV9maXQgPC0gbW9kZWxfdHJlZSAlPiUKICAgICAgICAgICAgICAgICAgIGZpdCgKICAgICAgICAgICAgICAgICAgICAgZm9ybXVsYSA9IHByaWNlIH4gLiwKICAgICAgICAgICAgICAgICAgICAgZGF0YSAgICA9IGRhdGFfdHJhaW5fcHJlcAogICAgICAgICAgICAgICAgICAgKQpgYGAKCmBgYHtyfQojIEVudHJlbmFtaWVudG8gZW1wbGVhbmRvIHggZSBZLgp2YXJpYWJsZV9yZXNwdWVzdGEgPC0gInByaWNlIgoKcHJlZGljb3JlcyA8LSBzZXRkaWZmKGNvbG5hbWVzKGRhdGFfdHJhaW5fcHJlcCksIHZhcmlhYmxlX3Jlc3B1ZXN0YSkKCm1vZGVsX3RyZWVfZml0IDwtIG1vZGVsX3RyZWUgJT4lCiAgICAgICAgICAgICAgICAgICBmaXRfeHkoCiAgICAgICAgICAgICAgICAgICAgIHggPSBkYXRhX3RyYWluX3ByZXBbLCBwcmVkaWNvcmVzXSwKICAgICAgICAgICAgICAgICAgICAgeSA9IGRhdGFfdHJhaW5fcHJlcFtbdmFyaWFibGVfcmVzcHVlc3RhXV0KICAgICAgICAgICAgICAgICAgICkKYGBgCgpQYXJhIHZlciBudWVzdHJvIG1vZGVsbyB1dGlsaXphbW9zIGxvIHF1ZSBzZSBhbG1hY2VuYSBkZW50cm8gZGUgYG1vZGVsX3RyZWVfZml0YCBjb21vIGBmaXRgOgoKYGBge3J9Cm1vZGVsX3RyZWVfZml0JGZpdApgYGAKCgojIyBWYWxpZGFjacOzbiBkZWwgbW9kZWxvCgpMbyBxdWUgYnVzY2Ftb3MgdmVyIGVzIHF1ZSB0YW4gY2VydGVyYSBlcyBsYSBwcmVkaWNjacOzbiBkZSBudWVzdHJvIG1vZGVsbywgeWEgc2VhIGNvbiBvYnNlcnZhY2lvbmVzIHF1ZSBubyBoYSAqdmlzdG8qIGVsIG1vZGVsbyB5IHBvciBlbmRlLCBjb24gb2JzZXJ2YWNpb25lcyBmdXR1cmFzLiAKCkVsIGVycm9yIG1vc3RyYWRvIHBvciBkZWZlY3RvIHRyYXMgZW50cmVuYXIgdW4gbW9kZWxvIHN1ZWxlIHNlciBlbCBlcnJvciBkZSBlbnRyZW5hbWllbnRvLCBlbCBlcnJvciBxdWUgY29tZXRlIGVsIG1vZGVsbyBhbCBwcmVkZWNpciBsYXMgb2JzZXJ2YWNpb25lcyBxdWUgeWEgaGEg4oCcdmlzdG/igJ0uIAoKRXN0b3MgZXJyb3JlcyBzb24gw7p0aWxlcyBwYXJhIGVudGVuZGVyIGPDs21vIGVzdMOhIGFwcmVuZGllbmRvIGVsIG1vZGVsbyAoZXN0dWRpbyBkZSByZXNpZHVvcyksIG5vIGVzIHVuYSBlc3RpbWFjacOzbiByZWFsaXN0YSBkZSBjw7NtbyBzZSBjb21wb3J0YSBlbCBtb2RlbG8gYW50ZSBudWV2YXMgb2JzZXJ2YWNpb25lcyAqKGVsIGVycm9yIGRlIGVudHJlbmFtaWVudG8gc3VlbGUgc2VyIGRlbWFzaWFkbyBvcHRpbWlzdGEpKi4gCgpQYXJhIGNvbnNlZ3VpciB1bmEgZXN0aW1hY2nDs24gbcOhcyBjZXJ0ZXJhLCB5IGFudGVzIGRlIHJlY3VycmlyIGFsIGNvbmp1bnRvIGRlICp0ZXN0Kiwgc2UgcHVlZGVuIGVtcGxlYXIgZXN0cmF0ZWdpYXMgZGUgdmFsaWRhY2nDs24gYmFzYWRhcyBlbiAqcmVzYW1wbGluZyouIEVsIHBhcXVldGUgYHJzYW1wbGVyYCBpbmNvcnBvcmEgbG9zIG3DqXRvZG9zIEJvb3RzdHJhcCAoYGJvb3RzdHJhcHNgKSwgVi1Gb2xkIENyb3NzLVZhbGlkYXRpb24geSBSZXBlYXRlZCBWLUZvbGQgQ3Jvc3MtVmFsaWRhdGlvbiAoYHZmb2xkX2N2YCksIE5lc3RlZCBvciBEb3VibGUgUmVzYW1wbGluZyAoYG5lc3RlZF9jdmApLCBHcm91cCBWLUZvbGQgQ3Jvc3MtVmFsaWRhdGlvbiAoYGdyb3VwX3Zmb2xkX2N2YCksIExlYXZlLU9uZS1PdXQgQ3Jvc3MtVmFsaWRhdGlvbiAoYGxvb19jdmApLCBNb250ZSBDYXJsbyBDcm9zcy1WYWxpZGF0aW9uKGBtY19jdmApLiBDYWRhIHVubyBmdW5jaW9uYSBpbnRlcm5hbWVudGUgZGUgZm9ybWEgZGlzdGludGEsIHBlcm8gdG9kb3MgZWxsb3Mgc2UgYmFzYW4gZW4gbGEgaWRlYTogKmFqdXN0YXIgeSBldmFsdWFyIGVsIG1vZGVsbyBkZSBmb3JtYSByZXBldGlkYSwgZW1wbGVhbmRvIGNhZGEgdmV6IGRpc3RpbnRvcyBzdWJjb25qdW50b3MgY3JlYWRvcyBhIHBhcnRpciBkZSBsb3MgZGF0b3MgZGUgZW50cmVuYW1pZW50byB5IG9idGVuaWVuZG8gZW4gY2FkYSByZXBldGljacOzbiB1bmEgZXN0aW1hY2nDs24gZGVsIGVycm9yIChzaW11bGFjaW9uZXMgZGUgbG9zIGRhdG9zKS4qIEVsIHByb21lZGlvIGRlIHRvZGFzIGxhcyBlc3RpbWFjaW9uZXMgdGllbmRlIGEgY29udmVyZ2VyIGVuIGVsIHZhbG9yIHJlYWwgZGVsIGVycm9yIGRlIHRlc3QuCgpTZSBhanVzdGEgZGUgbnVldm8gZWwgbW9kZWxvLCBlc3RhIHZleiBjb24gdmFsaWRhY2nDs24gY3J1emFkYSByZXBldGlkYSBwYXJhIGVzdGltYXIgc3UgZXJyb3IuCgpFbiBwcmltZXIgbHVnYXIsIHNlIGNyZWEgdW4gb2JqZXRvIGByZXNhbXBsZWAgcXVlIGNvbnRlbmdhIGxhIGluZm9ybWFjacOzbiBzb2JyZSBsYXMgb2JzZXJ2YWNpb25lcyBxdWUgZm9ybWFuIHBhcnRlIGRlIGNhZGEgcGFydGljacOzbi4gRGFkbyBxdWUgZWwgcmVwYXJ0byBlcyBhbGVhdG9yaW8sIGVzIGltcG9ydGFudGUgZW1wbGVhciB1bmEgc2VtaWxsYSBgc2V0LnNlZWQoKWAgcGFyYSBxdWUgbG9zIHJlc3VsdGFkb3Mgc2VhbiByZXByb2R1Y2libGVzLgoKYGBge3J9CnNldC5zZWVkKDEyMzQpCmN2X2ZvbGRzIDwtIHZmb2xkX2N2KAogICAgICAgICAgICAgIGRhdGEgICAgPSBkYXRhX3RyYWluLAogICAgICAgICAgICAgIHYgICAgICAgPSA1LAogICAgICAgICAgICAgIHJlcGVhdHMgPSAxMCwKICAgICAgICAgICAgICBzdHJhdGEgID0gcHJpY2UKICAgICAgICAgICAgKQpoZWFkKGN2X2ZvbGRzKQpgYGAKCkNvbiBsYSBmdW5jacOzbiBgZml0X3Jlc2FtcGxlcygpYCBlbCBtb2RlbG8gc2UgYWp1c3RhIHkgZXZhbMO6YSBjb24gY2FkYSB1bmEgZGUgbGFzIHBhcnRpY2lvbmVzIGRlIGZvcm1hIGF1dG9tw6F0aWNhLCBjYWxjdWxhbmRvIHkgYWxtYWNlbmFuZG8gZW4gY2FkYSBpdGVyYWNpw7NuIGxhIG3DqXRyaWNhIGRlIGludGVyw6lzLiBDb21vIHNlIGNvbWVudMOzIGFudGVyaW9ybWVudGUsIGN1YW5kbyBzZSByZWFsaXphbiB2YWxpZGFjaW9uZXMgcG9yICpyZXNhbXBsaW5nKiwgZWwgcHJlcHJvY2VzYWRvIGRlYmUgb2N1cnJpciBkZW50cm8gZGUgY2FkYSBpdGVyYWNpw7NuLiAKClBhcmEgc2VndWlyIGVzdGUgcHJpbmNpcGlvLCBsYXMgcGFydGljaW9uZXMgc2UgY3JlYW4gY29uIGVsIGNvbmp1bnRvIGRlIGRhdG9zIGRlIGVudHJlbmFtaWVudG8gKnNpbiBwcmVwcm9jZXNhciogeSBzZSBsZSBwYXNhIGFsIGFyZ3VtZW50byBwcmVwcm9jZXNzb3IgdW4gb2JqZXRvIHJlY2lwZSBxdWUgY29udGVuZ2EgbGEgZGVmaW5pY2nDs24gZGVsIHByZXByb2Nlc2Fkby4KCmBgYHtyfQptb2RlbF90cmVlIDwtIGRlY2lzaW9uX3RyZWUobW9kZSA9ICJyZWdyZXNzaW9uIikgJT4lCiAgICAgICAgICAgICAgIHNldF9lbmdpbmUoZW5naW5lID0gInJwYXJ0IikKCnZhbGlkYXRpb25fZml0IDwtIGZpdF9yZXNhbXBsZXMoCiAgICAgICAgICAgICAgICAgICAgb2JqZWN0ICAgICAgID0gbW9kZWxfdHJlZSwKICAgICAgICAgICAgICAgICAgICAjIEVsIG9iamV0byByZWNpcGUgbm8gdGllbmUgcXVlIGVzdGFyIGVudHJlbmFkbwogICAgICAgICAgICAgICAgICAgIHByZXByb2Nlc3NvciA9IHRyYW5zZm9ybWVyLAogICAgICAgICAgICAgICAgICAgICMgTGFzIHJlc2FtcGxlcyBzZSB0aWVuZW4gcXVlIGhhYmVyIGNyZWFkbyBjb24gbG9zIGRhdG9zIHNpbiAKICAgICAgICAgICAgICAgICAgICAjIHByZXJvY2VzYXIKICAgICAgICAgICAgICAgICAgICByZXNhbXBsZXMgICAgPSBjdl9mb2xkcywKICAgICAgICAgICAgICAgICAgICBtZXRyaWNzICAgICAgPSBtZXRyaWNfc2V0KHJtc2UsIG1hZSksCiAgICAgICAgICAgICAgICAgICAgY29udHJvbCAgICAgID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSkKICAgICAgICAgICAgICAgICAgKQoKaGVhZCh2YWxpZGF0aW9uX2ZpdCkKYGBgCgpMb3MgcmVzdWx0YWRvcyBkZSBgZml0X3Jlc2FtcGxlcygpYCBzZSBhbG1hY2VuYW4gZW4gZm9ybWEgZGUgW2B0aWJibGVgXShodHRwczovL3RpYmJsZS50aWR5dmVyc2Uub3JnLyksIGRvbmRlIGxhcyBjb2x1bW5hcyBjb250aWVuZW4gbGEgaW5mb3JtYWNpw7NuIHNvYnJlIGNhZGEgcGFydGljacOzbjogc3UgaWQsIGxhcyBvYnNlcnZhY2lvbmVzIHF1ZSBmb3JtYW4gcGFydGUsIGxhcyBtw6l0cmljYXMgY2FsY3VsYWRhcywgc2kgaGEgaGFiaWRvIGFsZ8O6biBlcnJvciBvIHdhcm5pbmcgZHVyYW50ZSBlbCBhanVzdGUsIHkgbGFzIHByZWRpY2Npb25lcyBkZSB2YWxpZGFjacOzbiBzaSBzZSBoYSBpbmRpY2FkbyBgY29udHJvbCA9IGNvbnRyb2xfcmVzYW1wbGVzKHNhdmVfcHJlZCA9IFRSVUUpYC4gTGEgaW5mb3JtYWNpw7NuIHB1ZWRlIGV4dHJhZXJzZSBoYWNpZW5kbyB1biBgdW5uZXN0KClgIGRlIGxhcyBjb2x1bW5hcyBvIGVtcGxlYW5kbyBsYXMgZnVuY2lvbmVzIGF1eGlsaWFyZXMgYGNvbGxlY3RfcHJlZGljdGlvbnMoKWAgeSBgY29sbGVjdF9tZXRyaWNzKClgLgoKYGBge3J9CiMgTcOpdHJpY2FzIHByb21lZGlvIGRlIHRvZGFzIGxhcyBwYXJ0aWNpb25lcwp2YWxpZGF0aW9uX2ZpdCAlPiUgY29sbGVjdF9tZXRyaWNzKHN1bW1hcml6ZSA9IFRSVUUpCmBgYApgYGB7cn0KIyBNw6l0cmljYXMgaW5kaXZpZHVhbGVzIGRlIGNhZGEgdW5hIGRlIGxhcyBwYXJ0aWNpb25lcwp2YWxpZGF0aW9uX2ZpdCAlPiUgY29sbGVjdF9tZXRyaWNzKHN1bW1hcml6ZSA9IEZBTFNFKSAlPiUgCiAgaGVhZCgpCmBgYAoKYGBge3J9CmxpYnJhcnkoImdncHViciIpCiMgVmFsb3JlcyBkZSB2YWxpZGFjacOzbiAobWFlIHkgcm1zZSkgb2J0ZW5pZG9zIGVuIGNhZGEgcGFydGljacOzbiB5IHJlcGV0aWNpw7NuLgpwMSA8LSBnZ3Bsb3QoCiAgICAgICAgZGF0YSA9IHZhbGlkYXRpb25fZml0ICU+JSBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXplID0gRkFMU0UpLAogICAgICAgIGFlcyh4ID0gLmVzdGltYXRlLCBmaWxsID0gLm1ldHJpYykpICsKICAgICAgZ2VvbV9kZW5zaXR5KGFscGhhID0gMC41KSArCiAgICAgIHRoZW1lX2J3KCkgCnAyIDwtIGdncGxvdCgKICAgICAgICBkYXRhID0gdmFsaWRhdGlvbl9maXQgJT4lIGNvbGxlY3RfbWV0cmljcyhzdW1tYXJpemUgPSBGQUxTRSksCiAgICAgICAgYWVzKHggPSAubWV0cmljLCB5ID0gLmVzdGltYXRlLCBmaWxsID0gLm1ldHJpYywgY29sb3IgPSAubWV0cmljKSkgKwogICAgICBnZW9tX2JveHBsb3Qob3V0bGllci5zaGFwZSA9IE5BLCBhbHBoYSA9IDAuMSkgKwogICAgICBnZW9tX2ppdHRlcih3aWR0aCA9IDAuMDUsIGFscGhhID0gMC4zKSArCiAgICAgIGNvb3JkX2ZsaXAoKSArCiAgICAgIHRoZW1lX2J3KCkgKwogICAgICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50aWNrcy54ID0gZWxlbWVudF9ibGFuaygpKQogIApnZ2FycmFuZ2UocDEsIHAyLCBucm93ID0gMiwgY29tbW9uLmxlZ2VuZCA9IFRSVUUsIGFsaWduID0gInYiKSAlPiUgCmFubm90YXRlX2ZpZ3VyZSgKICB0b3AgPSB0ZXh0X2dyb2IoIkRpc3RyaWJ1Y2nDs24gZXJyb3JlcyBkZSB2YWxpZGFjacOzbiBjcnV6YWRhIiwgc2l6ZSA9IDE1KQopCmBgYAoKRWwgKnJtc2UgKHJhw616IGRlbCBlcnJvciBtZWRpbyBjdWFkcsOhdGljbykqIHByb21lZGlvIGVzdGltYWRvIG1lZGlhbnRlIHZhbGlkYWNpw7NuIGNydXphZGEgcmVwZXRpZGEgZXMgZGUgKjY3OTkwKi4gRXN0ZSB2YWxvciBzZXLDoSBjb250cmFzdGFkbyBtw6FzIGFkZWxhbnRlIGN1YW5kbyBzZSBjYWxjdWxlIGVsIHJtc2UgZGVsIG1vZGVsbyBjb24gZWwgY29uanVudG8gZGUgdGVzdC4KCkFsbWFjZW5hciBsYXMgcHJlZGljY2lvbmVzIGRlIGNhZGEgcGFydGljacOzbiBlcyDDunRpbCBwYXJhIHBvZGVyIGV2YWx1YXIgbG9zIHJlc2lkdW9zIGRlbCBtb2RlbG8geSBkaWFnbm9zdGljYXIgYXPDrSBzdSBjb21wb3J0YW1pZW50by4KCmBgYHtyfQojIFByZWRpY2Npb25lcyBpbmRpdmlkdWFsZXMgZGUgY2FkYSBvYnNlcnZhY2nDs24uCiMgU2kgc3VtbWFyaXplID0gVFJVRSBzZSBhZ3JlZ2FuIHRvZG9zIGxvcyB2YWxvcmVzIHByZWRpY2hvcyBhIG5pdmVsIGRlCiMgb2JzZXJ2YWNpw7NuLgp2YWxpZGF0aW9uX2ZpdCAlPiUgY29sbGVjdF9wcmVkaWN0aW9ucyhzdW1tYXJpemUgPSBUUlVFKSAlPiUgaGVhZCgpCmBgYAoKYGBge3J9CnAxIDwtIGdncGxvdCgKICAgICAgICBkYXRhID0gdmFsaWRhdGlvbl9maXQgJT4lIGNvbGxlY3RfcHJlZGljdGlvbnMoc3VtbWFyaXplID0gVFJVRSksCiAgICAgICAgYWVzKHggPSBwcmljZSwgeSA9IC5wcmVkKQogICAgICApICsKICAgICAgZ2VvbV9wb2ludChhbHBoYSA9IDAuMykgKwogICAgICBnZW9tX2FibGluZShzbG9wZSA9IDEsIGludGVyY2VwdCA9IDAsIGNvbG9yID0gImZpcmVicmljayIpICsKICAgICAgbGFicyh0aXRsZSA9ICJWYWxvciBwcmVkaWNobyB2cyB2YWxvciByZWFsIikgKwogICAgICB0aGVtZV9idygpCgoKcDIgPC0gZ2dwbG90KAogICAgICAgIGRhdGEgPSB2YWxpZGF0aW9uX2ZpdCAlPiUgY29sbGVjdF9wcmVkaWN0aW9ucyhzdW1tYXJpemUgPSBUUlVFKSwKICAgICAgICBhZXMoeCA9IC5yb3csIHkgPSBwcmljZSAtIC5wcmVkKQogICAgICApICsKICAgICAgZ2VvbV9wb2ludChhbHBoYSA9IDAuMykgKwogICAgICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAgMCwgY29sb3IgPSAiZmlyZWJyaWNrIikgKwogICAgICBsYWJzKHRpdGxlID0gIlJlc2lkdW9zIGRlbCBtb2RlbG8iKSArCiAgICAgIHRoZW1lX2J3KCkKCnAzIDwtIGdncGxvdCgKICAgICAgICBkYXRhID0gdmFsaWRhdGlvbl9maXQgJT4lIGNvbGxlY3RfcHJlZGljdGlvbnMoc3VtbWFyaXplID0gVFJVRSksCiAgICAgICAgYWVzKHggPSBwcmljZSAtIC5wcmVkKQogICAgICApICsKICAgICAgZ2VvbV9kZW5zaXR5KCkgKyAKICAgICAgbGFicyh0aXRsZSA9ICJEaXN0cmlidWNpw7NuIHJlc2lkdW9zIGRlbCBtb2RlbG8iKSArCiAgICAgIHRoZW1lX2J3KCkKCnA0IDwtIGdncGxvdCgKICAgICAgICBkYXRhID0gdmFsaWRhdGlvbl9maXQgJT4lIGNvbGxlY3RfcHJlZGljdGlvbnMoc3VtbWFyaXplID0gVFJVRSksCiAgICAgICAgYWVzKHNhbXBsZSA9IHByaWNlIC0gLnByZWQpCiAgICAgICkgKwogICAgICBnZW9tX3FxKCkgKwogICAgIGdlb21fcXFfbGluZShjb2xvciA9ICJmaXJlYnJpY2siKSArCiAgICAgIGxhYnModGl0bGUgPSAiUS1RIHJlc2lkdW9zIGRlbCBtb2RlbG8iKSArCiAgICAgIHRoZW1lX2J3KCkKCmdnYXJyYW5nZShwbG90bGlzdCA9IGxpc3QocDEsIHAyLCBwMywgcDQpKSAlPiUKYW5ub3RhdGVfZmlndXJlKAogIHRvcCA9IHRleHRfZ3JvYigiRGlzdHJpYnVjacOzbiByZXNpZHVvcyIsIHNpemUgPSAxNSwgZmFjZSA9ICJib2xkIikKKQoKYGBgCgpDdWFuZG8gc2UgYXBsaWNhbiBtw6l0b2RvcyBkZSogKnJlc2FtcGxpbmcqIGRlYmVtb3MgdGVuZXIgZW4gY3VlbnRhIGRvcyBjb3NhczogCgoxLiBFbCBjb3N0ZSBjb21wdXRhY2lvbmFsIHF1ZSBpbXBsaWNhIGFqdXN0YXIgbcO6bHRpcGxlcyB2ZWNlcyB1biBtb2RlbG8sIGNhZGEgdmV6IGNvbiB1biBzdWJjb25qdW50byBkZSBkYXRvcyBkaXN0aW50bywgCgoyLiB5IGxhIHJlcHJvZHVjaWJpbGlkYWQgZW4gbGEgY3JlYWNpw7NuIGRlIGxhcyBwYXJ0aWNpb25lcy4KCmBmaXRfcmVzYW1wbGVzKClgIHBlcm1pdGUgcGFyYWxlbGl6YXIgZWwgcHJvY2VzbyBzaSBzZSByZWdpc3RyYSB1biAqYmFja2VkKiBwYXJhbGVsbywgcGFyYSBlbGxvIHVzYW1vcyBsYSBmdW5jacOzbiBgcmVzdGVyRG9QYXJhbGxlbCgpYCBkZWwgcGFxdWV0ZSBgZG9QYXJhbGxlbGAuCgpgYGB7cn0KbGlicmFyeSgiZG9QYXJhbGxlbCIpCnJlZ2lzdGVyRG9QYXJhbGxlbChjb3JlcyA9IGRldGVjdENvcmVzKCkgLSAxKQpzZXQuc2VlZCgyMDIwKQptb2RlbF90cmVlIDwtIGRlY2lzaW9uX3RyZWUobW9kZSA9ICJyZWdyZXNzaW9uIikgJT4lCiAgICAgICAgICAgICAgIHNldF9lbmdpbmUoZW5naW5lID0gInJwYXJ0IikKCnZhbGlkYWNpb25fZml0IDwtIGZpdF9yZXNhbXBsZXMoCiAgICAgICAgICAgICAgICAgICAgb2JqZWN0ICAgICAgID0gbW9kZWxfdHJlZSwKICAgICAgICAgICAgICAgICAgICBwcmVwcm9jZXNzb3IgPSB0cmFuc2Zvcm1lciwKICAgICAgICAgICAgICAgICAgICByZXNhbXBsZXMgICAgPSBjdl9mb2xkcywKICAgICAgICAgICAgICAgICAgICBtZXRyaWNzICAgICAgPSBtZXRyaWNfc2V0KHJtc2UsIG1hZSksCiAgICAgICAgICAgICAgICAgICAgY29udHJvbCAgICAgID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSkKICAgICAgICAgICAgICAgICAgKQoKc3RvcEltcGxpY2l0Q2x1c3RlcigpCmBgYAoKIyMgSGlwZXJwYXLDoW1ldHJvcyAodHVuaW5nKQoKTXVjaG9zIG1vZGVsb3MsIGVudHJlIGVsbG9zIGxvcyAqw6FyYm9sZXMgZGUgcmVncmVzacOzbiosIGNvbnRpZW5lbiBwYXLDoW1ldHJvcyAoZGF0b3MgZGUgZW50cmFkYSBhbCBtb2RlbG8pIHF1ZSBubyBwdWVkZW4gYXByZW5kZXJzZSBhIHBhcnRpciBkZSBsb3MgZGF0b3MgZGUgZW50cmVuYW1pZW50byB5LCBwb3IgbG8gdGFudG8sIGRlYmVuIGRlIHNlciBlc3RhYmxlY2lkb3MgcG9yIGVsICphbmFsaXN0YSouIEEgZXN0b3Mgc2UgbGVzIGNvbm9jZSBjb21vICpoaXBlcnBhcsOhbWV0cm9zKi4gKioqwqFPam8hKioqIG5vIHNlIHRyYXRhIGRlIHRlbmVyIHVuIG1vZGVsbyB5IGNvcnJlcmxvIHkgeWEgY29uIGVzdG8gbGxhbWFybm9zICpjaWVudMOtZmljb3MgZGUgZGF0b3MqLiAKCkxvcyByZXN1bHRhZG9zIGRlIHVuIG1vZGVsbyBwdWVkZW4gZGVwZW5kZXIgZW4gZ3JhbiBtZWRpZGEgZGVsIHZhbG9yIHF1ZSB0b21lbiBzdXMgaGlwZXJwYXLDoW1ldHJvcywgc2luIGVtYmFyZ28sIG5vIHNlIHB1ZWRlIGNvbm9jZXIgZGUgYW50ZW1hbm8gY3XDoWwgZXMgZWwgYWRlY3VhZG8uIEF1bnF1ZSBjb24gbGEgcHLDoWN0aWNhLCBsb3MgZXNwZWNpYWxpc3RhcyBlbiAqbWFjaGluZSBsZWFybmluZyogZ2FuYW4gaW50dWljacOzbiBzb2JyZSBxdcOpIHZhbG9yZXMgcHVlZGVuIGZ1bmNpb25hciBtZWpvciBlbiBjYWRhIHByb2JsZW1hLCBubyBoYXkgcmVnbGFzIGZpamFzLiBMYSBmb3JtYSBtw6FzIGNvbcO6biBkZSBlbmNvbnRyYXIgbG9zIHZhbG9yZXMgKsOzcHRpbW9zKiBlcyBwcm9iYW5kbyBkaWZlcmVudGVzIHBvc2liaWxpZGFkZXMgcG9yIG1lZGlvIGRlIGFsZ29yaXRtb3MgZGUgb3B0aW1pemFjacOzbi4KClBvZGVtb3Mgc2VndWlyIGxvcyBzaWd1aWVudGVzIHBhc29zOgoKMS4gRXNjb2dlciB1biBjb25qdW50byBkZSB2YWxvcmVzIHBhcmEgZWwgbyBsb3MgaGlwZXJwYXLDoW1ldHJvcy4KCjIuIFBhcmEgY2FkYSB2YWxvciAoY29tYmluYWNpw7NuIGRlIHZhbG9yZXMgc2kgaGF5IG3DoXMgZGUgdW4gaGlwZXJwYXLDoW1ldHJvKSwgZW50cmVuYXIgZWwgbW9kZWxvIHkgZXN0aW1hciBzdSBlcnJvciBtZWRpYW50ZSB1biBtw6l0b2RvIGRlIHZhbGlkYWNpw7NuLgoKMy4gQWp1c3RhciBkZSBudWV2byBlbCBtb2RlbG8sIGVzdGEgdmV6IGNvbiB0b2RvcyBsb3MgZGF0b3MgZGUgZW50cmVuYW1pZW50byB5IGNvbiBsb3MgbWVqb3JlcyBoaXBlcnBhcsOhbWV0cm9zIGVuY29udHJhZG9zLgoKRWwgbW9kZWxvIGBkZWNpc2lvbl90cmVlYCBlbXBsZWFkbyBoYXN0YSBhaG9yYSB0aWVuZW4gdHJlcyBoaXBlcnBhcsOhbWV0cm9zOiBgY29zdF9jb21wbGV4aXR5YCwgYHRyZWVfZGVwdGhgIHkgYG1pbl9uYC4gVG9kb3MgZWxsb3MgY29udHJvbGFuIGVsICp0YW1hw7FvIGRlbCDDoXJib2wgZmluYWwqLiBQb3IgZWplbXBsbywgY3VhbnRvIG1heW9yIGVsICB2YWxvciBkZSBgdHJlZV9kZXB0aGAsIG3DoXMgcmFtaWZpY2FjaW9uZXMgdGllbmUgZWwgw6FyYm9sIHkgbcOhcyBmbGV4aWJsZSBlcyBlbCBtb2RlbG8uIEVzdG8gbGUgcGVybWl0ZSBhanVzdGFyc2UgbWVqb3IgYSBsYXMgb2JzZXJ2YWNpb25lcyBkZSBlbnRyZW5hbWllbnRvIHBlcm8gY29uIGVsIHJpZXNnbyBkZSAqb3ZlcmZpdHRpbmcqLiBDb24gbGFzIGZ1bmNpb25lcyBgdHVuZSgpYCB5IGB0dW5lX2dyaWQoKWAgZGVsIHBhcXVldGUgYHR1bmVgIHNlIHB1ZWRlbiBleHBsb3JhciBkaWZlcmVudGVzIHZhbG9yZXMgZGUgaGlwZXJwYXLDoW1ldHJvcyBzaW4gYXBlbmFzIHRlbmVyIHF1ZSBjYW1iaWFyIGxhIGVzdHJ1Y3R1cmEgZGVsIGPDs2RpZ28uCgo8Y2VudGVyPgohW10oLi9pbWFnZXMvdHVuZS5wbmcpe3dpZHRoPTE1JX0KPC9jZW50ZXI+CgpTZSB2dWVsdmUgYSBhanVzdGFyIHVuIG1vZGVsbyBgZGVjaXNpb25fdHJlZWAgY29uIGRpZmVyZW50ZXMgdmFsb3JlcyBkZSBgdHJlZV9kZXB0aGAgeSBgbWluX25gLCB5IGVtcGxlYW5kbyB2YWxpZGFjacOzbiBjcnV6YWRhIHJlcGV0aWRhIHBhcmEgaWRlbnRpZmljYXIgY29uIGN1w6FsZXMgZGUgZWxsb3Mgc2Ugb2J0aWVuZW4gbWVqb3JlcyByZXN1bHRhZG9zLgoKYGBge3J9CiMgRGVmaW5pY2nDs24gZGVsIG1vZGVsbyB5IGxvcyBoaXBlcnBhcsOhbWV0cm9zIGEgb3B0aW1pemFyCm1vZGVsX3RyZWUgPC0gZGVjaXNpb25fdHJlZSgKICAgICAgICAgICAgICAgICBtb2RlICAgICAgID0gInJlZ3Jlc3Npb24iLAogICAgICAgICAgICAgICAgIHRyZWVfZGVwdGggPSB0dW5lKCksCiAgICAgICAgICAgICAgICAgbWluX24gICAgICA9IHR1bmUoKQogICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICBzZXRfZW5naW5lKGVuZ2luZSA9ICJycGFydCIpCgojIFZhbGlkYWNpw7NuIHkgY3JlYWNpw7NuIGRlIHBhcnRpY2lvbmVzCnNldC5zZWVkKDEyMzQpCmN2X2ZvbGRzIDwtIHZmb2xkX2N2KAogICAgICAgICAgICAgIGRhdGEgICAgPSBkYXRhX3RyYWluLAogICAgICAgICAgICAgIHYgICAgICAgPSA1LAogICAgICAgICAgICAgIHN0cmF0YSAgPSBwcmljZQogICAgICAgICAgICAgKQoKIyBPcHRpbWl6YWNpw7NuIGRlIGhpcGVycGFyw6FtZXRyb3MKcmVnaXN0ZXJEb1BhcmFsbGVsKGNvcmVzID0gcGFyYWxsZWw6OmRldGVjdENvcmVzKCkgLSAxKQoKZ3JpZF9maXQgPC0gdHVuZV9ncmlkKAogICAgICAgICAgICAgIG9iamVjdCAgICAgICA9IG1vZGVsX3RyZWUsCiAgICAgICAgICAgICAgIyBFbCBvYmpldG8gcmVjaXBlIG5vIHRpZW5lIHF1ZSBlc3RhciBlbnRyZW5hZG8KICAgICAgICAgICAgICBwcmVwcm9jZXNzb3IgPSB0cmFuc2Zvcm1lciwKICAgICAgICAgICAgICAjIExhcyByZXNhbXBsZXMgc2UgdGllbmVuIHF1ZSBoYWJlciBjcmVhZG8gY29uIGxvcyBkYXRvcyBzaW4gCiAgICAgICAgICAgICAgIyBwcmVyb2Nlc2FyCiAgICAgICAgICAgICAgcmVzYW1wbGVzICAgID0gY3ZfZm9sZHMsCiAgICAgICAgICAgICAgbWV0cmljcyAgICAgID0gbWV0cmljX3NldChybXNlLCBtYWUpLAogICAgICAgICAgICAgIGNvbnRyb2wgICAgICA9IGNvbnRyb2xfZ3JpZChzYXZlX3ByZWQgPSBUUlVFKSwKICAgICAgICAgICAgICAjIE7Dum1lcm8gZGUgY29tYmluYWNpb25lcyBnZW5lcmFkYXMgYXV0b23DoXRpY2FtZW50ZQogICAgICAgICAgICAgIGdyaWQgICAgICAgICA9IDcwCiAgICAgICAgICAgICkKc3RvcEltcGxpY2l0Q2x1c3RlcigpCmBgYAoKTG9zIHJlc3VsdGFkb3MgZGUgbGEgYsO6c3F1ZWRhIGRlIGhpcGVycGFyw6FtZXRyb3MgcHVlZGVuIHZlcnNlIGhhY2llbmRvIHVuICp1bm5lc3QoKSogZGUgZWwgKnRpYmJsZSogcXVlIHNlIGNyZWEgZGUgbGEgZWplY3VjacOzbiBgZ3JpZF9maXRgLCBvIGNvbiBsYXMgZnVuY2lvbmVzIGF1eGlsaWFyZXMgYGNvbGxlY3RfbWV0cmljcygpYCwgYGNvbGxlY3RfcHJlZGljdGlvbnNgLCBgc2hvd19iZXN0KClgIHkgYHNlbGVjdF9iZXN0KClgLgoKYGBge3J9CmdyaWRfZml0ICU+JSB1bm5lc3QoLm1ldHJpY3MpICU+JSBoZWFkKCkKYGBgCgpgYGB7cn0KZ3JpZF9maXQgJT4lIGNvbGxlY3RfbWV0cmljcyhzdW1tYXJpemUgPSBUUlVFKSAlPiUgaGVhZCgpCmBgYAoKYGBge3J9CmdyaWRfZml0ICU+JSBzaG93X2Jlc3QobWV0cmljID0gInJtc2UiLCBuID0gNSkKYGBgCkVzIGNvbnZlbmllbnRlIHZpc3VhbGl6YXIgbGEgZXZvbHVjacOzbiBkZWwgZXJyb3IgZW4gZnVuY2nDs24gZGUgbG9zIGhpcGVycGFyw6FtZXRyb3MuICBTaW4gZW1iYXJnbywgaGF5IHF1ZSB0ZW5lciBlbiBjdWVudGEgcXVlIGVzdG9zIG5vIHNvbiBpbmRlcGVuZGllbnRlcyBsb3MgdW5vcyBkZSBsb3Mgb3Ryb3MsIGVsIGNvbXBvcnRhbWllbnRvIGRlIHVuIGhpcGVycGFyw6FtZXRybyBwdWVkZSBjYW1iaWFyIG11Y2hvIGRlcGVuZGllbmRvIGRlbCB2YWxvciBxdWUgdG9tZW4gbG9zIG90cm9zLgoKYGBge3J9CmdyaWRfZml0ICU+JQogIGNvbGxlY3RfbWV0cmljcyhzdW1tYXJpemUgPSBUUlVFKSAlPiUKICBmaWx0ZXIoLm1ldHJpYyA9PSAicm1zZSIpICU+JQogIHNlbGVjdCgtYyguZXN0aW1hdG9yLCBuKSkgJT4lCiAgcGl2b3RfbG9uZ2VyKAogICAgY29scyA9IGModHJlZV9kZXB0aCwgbWluX24pLAogICAgdmFsdWVzX3RvID0gInZhbHVlIiwKICAgIG5hbWVzX3RvID0gInBhcmFtZXRlciIKICApICU+JQogIGdncGxvdChhZXMoeCA9IHZhbHVlLCB5ID0gbWVhbiwgY29sb3IgPSBwYXJhbWV0ZXIpKSArCiAgZ2VvbV9wb2ludCgpICsKICBnZW9tX2xpbmUoKSArIAogIGxhYnModGl0bGUgPSAiRXZvbHVjacOzbiBkZWwgZXJyb3IgZW4gZnVuY2nDs24gZGUgbG9zIGhpcGVycGFyw6FtZXRyb3MiKSArCiAgZmFjZXRfd3JhcChmYWNldHMgPSB2YXJzKHBhcmFtZXRlciksIG5yb3cgPSAyLCBzY2FsZXMgPSAiZnJlZSIpICsKICB0aGVtZV9idygpICsgCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQpgYGAKCmBgYHtyfQpncmlkX2ZpdCAlPiUKICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXplID0gVFJVRSkgJT4lCiAgZmlsdGVyKC5tZXRyaWMgPT0gInJtc2UiKSAlPiUKICBzZWxlY3QoLWMoLmVzdGltYXRvciwgbikpICU+JQogIGdncGxvdChhZXMoeCA9IHRyZWVfZGVwdGgsIHkgPSBtaW5fbiwgY29sb3IgPSBtZWFuLCBzaXplID0gbWVhbikpICsKICBnZW9tX3BvaW50KCkgKwogIHNjYWxlX2NvbG9yX3ZpcmlkaXNfYygpICsKICBsYWJzKHRpdGxlID0gIkV2b2x1Y2nDs24gZGVsIGVycm9yIGVuIGZ1bmNpw7NuIGRlIGxvcyBoaXBlcnBhcsOhbWV0cm9zIikgKwogIHRoZW1lX2J3KCkKYGBgCgpFbiBlc3RlIGNhc28sIHBhcmVjZSBxdWUgZWwgZXJyb3IgZGVsIG1vZGVsbyBzZSByZWR1Y2UgYSBtZWRpZGEgcXVlIGVsIHZhbG9yIGRlIGBtaW5fbmAgYXVtZW50YSAoYXVucXVlIG5vIGVzIGVzdGFibGUpIHkgdGFtYmnDqW4gZWwgZGUgYHRyZWVfZGVwdGhgLiBFbiBlc3RlIMO6bHRpbW8gbGEgbWVqb3JhIHBhcmVjZSBlc3RhYmlsaXphcnNlIGEgcGFydGlyIGRlIDQuCgpBdW5xdWUgbG9zIGF1dG9yZXMgZGUgYHRpZHltb2RlbHNgIGhhbiBlc3RhYmxlY2lkbyBwb3IgZGVmZWN0byB2YWxvcmVzIGFkZWN1YWRvcyBiYXNhZG9zIGVuIHN1IGFtcGxpYSBleHBlcmllbmNpYSwgZW4gbXVjaG9zIGNhc29zIGVzIGNvbnZlbmllbnRlIHRlbmVyIG3DoXMgY29udHJvbCBzb2JyZSBsYSBiw7pzcXVlZGEsIHlhIHF1ZSBsYSBzZWxlY2Npw7NuIHNlIGhhY2UgZGUgZm9ybWEgYXV0b23DoXRpY2EuIFBhcmEgY29uc2VndWlyIGVzdGUgY29udHJvbCwgcG9kZW1vcyB1dGlsaXphciAgYGV4cGFuZC5ncmlkKClgIChwYXJhIGVzcGVjaWZpY2FyIGV4YWN0YW1lbnRlIHF1w6kgdmFsb3JlcyBzZSBlbXBsZWFuKSwgbyBjb24gbGFzIGZ1bmNpw7NuZXMgYHJlZ3VsYXJfZ3JpZCgpYCwgYGdyaWRfcmFuZG9tKClgLCBgZ3JpZF9tYXhfZW50cm9weSgpYCwgeSBgZ3JpZF9sYXRpbl9oeXBlcmN1YmUoKWAgZGVsIHBhcXVldGUgYGRpYWxzYC4KCjxjZW50ZXI+CiFbXSguL2ltYWdlcy9kaWFscy5wbmcpe3dpZHRoPTE1JX0KPC9jZW50ZXI+CgpTZSByZXBpdGUgbGEgYsO6c3F1ZWRhIGRlIG1lam9yZXMgaGlwZXJwYXLDoW1ldHJvcyBwZXJvIGVzdGEgdmV6IGRlZmluaWVuZG8gZWwgZXNwYWNpbyBkZSBiw7pzcXVlZGEuCgpgYGB7cn0KIyBEZWZpbmljacOzbiBkZWwgbW9kZWxvIHkgZGUgbG9zIGhpcGVycGFyw6FtZXRyb3MgYSBvcHRpbWl6YXIKbW9kZWxfdHJlZSA8LSBkZWNpc2lvbl90cmVlKAogICAgICAgICAgICAgICAgIG1vZGUgICAgICAgPSAicmVncmVzc2lvbiIsCiAgICAgICAgICAgICAgICAgdHJlZV9kZXB0aCA9IHR1bmUoKSwKICAgICAgICAgICAgICAgICBtaW5fbiAgICAgID0gdHVuZSgpCiAgICAgICAgICAgICAgICkgJT4lCiAgICAgICAgICAgICAgIHNldF9lbmdpbmUoZW5naW5lID0gInJwYXJ0IikKCiMgVmFsaWRhY2nDs24geSBjcmVhY2nDs24gZGUgcGFydGljaW9uZXMKc2V0LnNlZWQoMTIzNCkKY3ZfZm9sZHMgPC0gdmZvbGRfY3YoCiAgICAgICAgICAgICAgZGF0YSAgICA9IGRhdGFfdHJhaW4sCiAgICAgICAgICAgICAgdiAgICAgICA9IDUsCiAgICAgICAgICAgICAgc3RyYXRhICA9IHByaWNlCiAgICAgICAgICAgICApCgojIEdyaWQgZGUgSGlwZXJwYXLDoW1ldHJvcwpzZXQuc2VlZCgxMjM0KQpoaXBlcnBhcl9ncmlkIDwtIGdyaWRfcmFuZG9tKAogICAgICAgICAgICAgICAgICAjIFJhbmdvIGRlIGLDunNxdWVkYSBwYXJhIGNhZGEgaGlwZXJwYXLDoW1ldHJvCiAgICAgICAgICAgICAgICAgIHRyZWVfZGVwdGgocmFuZ2UgPSBjKDEsIDEwKSwgdHJhbnMgPSBOVUxMKSwKICAgICAgICAgICAgICAgICAgbWluX24ocmFuZ2UgICAgICA9IGMoMiwgMTAwKSwgdHJhbnMgPSBOVUxMKSwKICAgICAgICAgICAgICAgICAgIyBOw7ptZXJvIGNvbWJpbmFjaW9uZXMgYWxlYXRvcmlhcyBwcm9iYWRhcwogICAgICAgICAgICAgICAgICBzaXplID0gNTAKICAgICAgICAgICAgICAgICkKCiMgRWplY3VjacOzbiB5IG9wdGltaXphY2nDs24gZGUgaGlwZXJwYXLDoW1ldHJvcwpyZWdpc3RlckRvUGFyYWxsZWwoY29yZXMgPSBwYXJhbGxlbDo6ZGV0ZWN0Q29yZXMoKSAtIDEpCgpncmlkX2ZpdCA8LSB0dW5lX2dyaWQoCiAgICAgICAgICAgICAgb2JqZWN0ICAgICAgID0gbW9kZWxfdHJlZSwKICAgICAgICAgICAgICAjIEVsIG9iamV0byByZWNpcGUgbm8gdGllbmUgcXVlIGVzdGFyIGVudHJlbmFkbwogICAgICAgICAgICAgIHByZXByb2Nlc3NvciA9IHRyYW5zZm9ybWVyLAogICAgICAgICAgICAgICMgTGFzIHJlc2FtcGxlcyBzZSB0aWVuZW4gcXVlIGhhYmVyIGNyZWFkbyBjb24gbG9zIGRhdG9zIHNpbiAKICAgICAgICAgICAgICAjIHByZXJvY2VzYXIKICAgICAgICAgICAgICByZXNhbXBsZXMgICAgPSBjdl9mb2xkcywKICAgICAgICAgICAgICBtZXRyaWNzICAgICAgPSBtZXRyaWNfc2V0KHJtc2UsIG1hZSksCiAgICAgICAgICAgICAgY29udHJvbCAgICAgID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSksCiAgICAgICAgICAgICAgIyBIaXBlcnBhcsOhbWV0cm9zCiAgICAgICAgICAgICAgZ3JpZCAgICAgICAgID0gaGlwZXJwYXJfZ3JpZAogICAgICAgICAgICApCnN0b3BJbXBsaWNpdENsdXN0ZXIoKQpgYGAKCkFob3JhIHZlbW9zIGxvcyByZXN1bHRhZG9zOgoKYGBge3J9CmdyaWRfZml0ICU+JSBzaG93X2Jlc3QobWV0cmljID0gInJtc2UiLCBuID0gNSkKYGBgCmBgYHtyfQpncmlkX2ZpdCAlPiUKICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXplID0gVFJVRSkgJT4lCiAgZmlsdGVyKC5tZXRyaWMgPT0gInJtc2UiKSAlPiUKICBzZWxlY3QoLWMoLmVzdGltYXRvciwgbikpICU+JQogIHBpdm90X2xvbmdlcigKICAgIGNvbHMgPSBjKHRyZWVfZGVwdGgsIG1pbl9uKSwKICAgIHZhbHVlc190byA9ICJ2YWx1ZSIsCiAgICBuYW1lc190byA9ICJwYXJhbWV0ZXIiCiAgKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSB2YWx1ZSwgeSA9IG1lYW4sIGNvbG9yID0gcGFyYW1ldGVyKSkgKwogIGdlb21fcG9pbnQoKSArCiAgZ2VvbV9saW5lKCkgKyAKICBsYWJzKHRpdGxlID0gIkV2b2x1Y2nDs24gZGVsIGVycm9yIGVuIGZ1bmNpw7NuIGRlIGxvcyBoaXBlcnBhcsOhbWV0cm9zIikgKwogIGZhY2V0X3dyYXAoZmFjZXRzID0gdmFycyhwYXJhbWV0ZXIpLCBucm93ID0gMiwgc2NhbGVzID0gImZyZWUiKSArCiAgdGhlbWVfYncoKSArIAogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikKYGBgCgpMb3MgbWVqb3JlcyByZXN1bHRhZG9zLCBlbiBiYXNlIGFsIHJtc2Ugc2UgaGFuIG9idGVuaWRvIGNvbiBsb3MgaGlwZXJwYXLDoW1ldHJvczogCgpgYGB7cn0KIyBTZWxlY2Npw7NuIGRlIGxvcyBtZWpvcmVzIGhpcGVycGFyw6FtZXRyb3MgZW5jb250cmFkb3MKYmVzdF9oaXAgPC0gc2VsZWN0X2Jlc3QoZ3JpZF9maXQsIG1ldHJpYyA9ICJybXNlIikKYmVzdF9oaXAKYGBgCgojIyBNb2RlbG8gZmluYWwKCllhIGhhY2llbmRvIGxhIG9wdGltaXphY2nDs24gZGUgbG9zIGhpcGVycGFyw6FtZXRyb3MsIGVudHJhbW9zIGFsIG1vZGVsbyBwZXJvIGNvbiBsb3MgZGF0b3MgZGUgZW50cmVuYW1pZW50byBjb24gZXN0b3MgcGFyw6FtZXRyb3MuCgpgYGB7cn0KZmluYWxfbW9kZWxfdHJlZSA8LSBmaW5hbGl6ZV9tb2RlbCh4ID0gbW9kZWxfdHJlZSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFyYW1ldGVycyA9IGJlc3RfaGlwKQpmaW5hbF9tb2RlbF90cmVlCmBgYApgYGB7cn0KZmluYWxfbW9kZWxfdHJlZV9maXQgIDwtIGZpbmFsX21vZGVsX3RyZWUgJT4lCiAgICAgICAgICAgICAgICAgICAgICAgICAgZml0KAogICAgICAgICAgICAgICAgICAgICAgICAgICAgZm9ybXVsYSA9IHByaWNlIH4gLiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdGEgICAgPSBkYXRhX3RyYWluX3ByZXAKICAgICAgICAgICAgICAgICAgICAgICAgICApCmBgYAoKIyMgUHJlZGljY2nDs24KClVuYSB2ZXogcXVlIGVsIG1vZGVsbyBmaW5hbCBoYSBzaWRvIGFqdXN0YWRvLCBjb24gbGEgZnVuY2nDs24gYHByZWRpY3QoKWAgc2UgcHVlZGVuIHByZWRlY2lyIG51ZXZvcyBkYXRvcy4gTG9zIGFyZ3VtZW50b3MgZGUgbGEgZnVuY2nDs24gc29uOgoKKiBgb2JqZWN0YDogdW4gbW9kZWxvIGVudHJlbmFkby4KCiogYG5ld2RhdGFgOiB1biBkYXRhZnJhbWUgY29uIG51ZXZhcyBvYnNlcnZhY2lvbmVzLgoKKiBgdHlwZWA6IHRpcG8gZGUgcHJlZGljY2nDs24gKGAibnVtZXJpYyJgLCBgImNsYXNzImAsIGAicHJvYiJgLCBgImNvbmZfaW50ImAsIGAicHJlZF9pbnQiYCwgYCJxdWFudGlsZSJgLCBvciBgInJhdyJgKS4KCmBgYHtyfQpwcmVkaWNjaW9uZXMgPC0gZmluYWxfbW9kZWxfdHJlZV9maXQgICU+JQogICAgICAgICAgICAgICAgcHJlZGljdCgKICAgICAgICAgICAgICAgICAgbmV3X2RhdGEgPSBkYXRhX3Rlc3RfcHJlcCwKICAgICAgICAgICAgICAgICAgI25ld19kYXRhID0gYmFrZSh0cmFuc2Zvcm1lcl9maXQsIGRhdG9zX3Rlc3QpLAogICAgICAgICAgICAgICAgICB0eXBlID0gIm51bWVyaWMiCiAgICAgICAgICAgICAgICApCnByZWRpY2Npb25lcyAlPiUgaGVhZCgpCmBgYAoKIyMgRXJyb3IgZGUgdGVzdAoKWWEgaGVtb3MgdmFsaWRhZG8gY29uIG1vZGVsb3MgZGUgdmFsaWRhY2nDs24gIChDViwgYm9vdHN0cmFwcGluZ+KApikgeSBzZSBoYW4gY29uc2VndWlkbyBlc3RpbWFjaW9uZXMgZGVsIGVycm9yIGRlbCBtb2RlbG8gYWwgcHJlZGVjaXIgbnVldmFzIG9ic2VydmFjaW9uZXMuIFBlcm8gbGEgbWVqb3IgZm9ybWEgZGUgZXZhbHVhciBlbCBtb2RlbG8gZmluYWwgZXMgcHJlZGljaWVuZG8gdW4gY29uanVudG8gdGVzdCwgZXMgZGVjaXIsIHV0aWxpemFuZG8gdW4gY29uanVudG8gZGUgZGF0b3MgcXVlIG5vIGhlbW9zIHVzYWRvIGVuIGVsIGVudHJlbmFtaWVudG8geSBlbiBsYSBvcHRpbWl6YWNpw7NuLiAgRWwgcGFxdWV0ZSBbYHlhcmRzdGlja2BdKGh0dHBzOi8veWFyZHN0aWNrLnRpZHltb2RlbHMub3JnLykgdGllbmUgZnVuY2lvbmVzIHByZWRlZmluaWRhcyBwYXJhIG3DqXRyaWNhcyBlbiBjdWVzdGnDs24gcXVlIG5vcyBwdWVkZW4gYXl1ZGFyLgoKPGNlbnRlcj4KIVtdKC4vaW1hZ2VzL3lhcmRzdGljay5wbmcpe3dpZHRoPTE1JX0KPC9jZW50ZXI+CgoKYGBge3J9CnByZWRpY2Npb25lcyA8LSBwcmVkaWNjaW9uZXMgJT4lIAogICAgICAgICAgICAgICAgYmluZF9jb2xzKGRhdGFfdGVzdF9wcmVwICU+JSBzZWxlY3QocHJpY2UpKQoKcm1zZShwcmVkaWNjaW9uZXMsIHRydXRoID0gcHJpY2UsIGVzdGltYXRlID0gLnByZWQsIG5hX3JtID0gVFJVRSkKYGBgCkVuIGVsIGFwYXJ0YWRvIGRlIG9wdGltaXphY2nDs24sIHNlIGVzdGltw7MsIG1lZGlhbnRlIHZhbGlkYWNpw7NuIGNydXphZGEgcmVwZXRpZGEsIHF1ZSBlbCAqcm1zZSogZGVsIG1vZGVsbyAqbW9kZWxfdHJlZSogZXJhIGRlIDY3NzkwLCB1biB2YWxvciBwcsOzeGltbyBhbCBvYnRlbmlkbyBjb24gZWwgY29uanVudG8gZGUgdGVzdCBiYWphIGEgKioqNjE1MzYqKiouCgojIFdvcmtmbG93cwoKTG9zIGB3b3JrZmxvd3NgIHBlcm1pdGVuIGNvbWJpbmFyIGVuIHVuIHPDs2xvIG9iamV0byB0b2RvcyBsb3MgZWxlbWVudG9zIHF1ZSBzZSBlbmNhcmdhbiBkZWwgcHJvY2VzYW1pZW50byAoYHJlY2lwZXNgKSwgbW9kZWxhZG8gKGBwYXJzbmlwYCB5IGB0dW5lYCkgeSBwb3N0LXByb2Nlc2Fkby4gUGFyYSBjcmVhciBlbCBgd29ya2Zsb3dzYCBzZSB2YW4gZW5jYWRlbmFuZG8gbG9zIGVsZW1lbnRvcyBjb24gbGFzIGZ1bmNpb25lcyBgYWRkXypgIG8gbW9kaWZpY2FuZG8gbG9zIGVsZW1lbnRvcyBkZSBsYXMgZnVuY2lvbmVzIGB1cGRhdGVfKmAuCjxjZW50ZXI+CiFbXSguL2ltYWdlcy93b3JrZmxvd3MucG5nKXt3aWR0aD0xNSV9CjwvY2VudGVyPgoKYGBge3J9CiMgRGVmaW5pY2nDs24gZGVsIG1vZGVsbyB5IGxvcyBoaXBlcnBhcsOhbWV0cm9zIGEgb3B0aW1pemFyCm1vZGVsX3RyZWUgPC0gZGVjaXNpb25fdHJlZSgKICAgICAgICAgICAgICAgICBtb2RlICAgICAgID0gInJlZ3Jlc3Npb24iLAogICAgICAgICAgICAgICAgIHRyZWVfZGVwdGggPSB0dW5lKCksCiAgICAgICAgICAgICAgICAgbWluX24gICAgICA9IHR1bmUoKQogICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICBzZXRfZW5naW5lKGVuZ2luZSA9ICJycGFydCIpCgojIERlZmluaWNpw7NuIGRlIHByb2Nlc2Fkbwp0cmFuc2Zvcm1lciA8LSByZWNpcGUoCiAgICAgICAgICAgICAgICAgIGZvcm11bGEgPSBwcmljZSB+IC4sCiAgICAgICAgICAgICAgICAgIGRhdGEgPSAgZGF0YV90cmFpbgogICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICBzdGVwX25hb21pdChhbGxfcHJlZGljdG9ycygpKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9uenYoYWxsX3ByZWRpY3RvcnMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfY2VudGVyKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfc2NhbGUoYWxsX251bWVyaWMoKSwgLWFsbF9vdXRjb21lcygpKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9kdW1teShhbGxfbm9taW5hbCgpLCAtYWxsX291dGNvbWVzKCkpCgojIEVzdHJhdGVnaWEgZGUgdmFsaWRhY2nDs24geSBjcmVhY2nDs24gZGUgcGFydGljaW9uZXMKc2V0LnNlZWQoMTIzNCkKY3ZfZm9sZHMgPC0gdmZvbGRfY3YoCiAgICAgICAgICAgICAgZGF0YSAgICA9IGRhdGFfdHJhaW4sCiAgICAgICAgICAgICAgdiAgICAgICA9IDUsCiAgICAgICAgICAgICAgc3RyYXRhICA9IHByaWNlCiAgICAgICAgICAgICApCgojIFdvcmtmbG93CndvcmtmbG93X21vZGVsYWRvIDwtIHdvcmtmbG93KCkgJT4lCiAgICAgICAgICAgICAgICAgICAgIGFkZF9yZWNpcGUodHJhbnNmb3JtZXIpICU+JQogICAgICAgICAgICAgICAgICAgICBhZGRfbW9kZWwobW9kZWxfdHJlZSkKCndvcmtmbG93X21vZGVsYWRvCmBgYAoKRWwgb2JqZXRvIGB3b3JrZmxvd2AgcHVlZGUgYWp1c3RhcnNlIGRpcmVjdGFtZW50ZSBjb24gbGEgZnVuY2nDs24gYGZpdCgpYCBvIGhhY2VyICp0dW5pbmcqIGRlIGhpcGVycGFyw6FtZXRyb3MgY29uIGB0dW5lX2dyaWQoKWAuCgpgYGB7cn0KIyBHcmlkIGRlIGhpcGVycGFyw6FtZXRyb3MKaGlwZXJwYXJfZ3JpZCA8LSBncmlkX3JlZ3VsYXIoCiAgICAgICAgICAgICAgICAgICMgUmFuZ28gZGUgYsO6c3F1ZWRhIHBhcmEgY2FkYSBoaXBlcnBhcsOhbWV0cm8KICAgICAgICAgICAgICAgICAgdHJlZV9kZXB0aChyYW5nZSA9IGMoMSwgMTApLCB0cmFucyA9IE5VTEwpLAogICAgICAgICAgICAgICAgICBtaW5fbihyYW5nZSA9IGMoMiwgMTAwKSwgdHJhbnMgPSBOVUxMKSwKICAgICAgICAgICAgICAgICAgIyBOw7ptZXJvIHZhbG9yZXMgcG9yIGhpcGVycGFyw6FtZXRybwogICAgICAgICAgICAgICAgICBsZXZlbHMgPSBjKDMsIDMpCiAgICAgICAgICAgICAgICApCgojIEVqZWN1Y2nDs24gb3B0aW1pemFjacOzbiBoaXBlcnBhcsOhbWV0cm9zCnJlZ2lzdGVyRG9QYXJhbGxlbChjb3JlcyA9IHBhcmFsbGVsOjpkZXRlY3RDb3JlcygpIC0gMSkKCmdyaWRfZml0IDwtIHR1bmVfZ3JpZCgKICAgICAgICAgICAgICBvYmplY3QgICAgICAgPSB3b3JrZmxvd19tb2RlbGFkbywKICAgICAgICAgICAgICByZXNhbXBsZXMgICAgPSBjdl9mb2xkcywKICAgICAgICAgICAgICBtZXRyaWNzICAgICAgPSBtZXRyaWNfc2V0KHJtc2UsIG1hZSksCiAgICAgICAgICAgICAgY29udHJvbCAgICAgID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSksCiAgICAgICAgICAgICAgIyBIaXBlcnBhcsOhbWV0cm9zCiAgICAgICAgICAgICAgZ3JpZCAgICAgICAgID0gaGlwZXJwYXJfZ3JpZAogICAgICAgICAgICApCnN0b3BJbXBsaWNpdENsdXN0ZXIoKQoKIyBFbnRyZW5hbWllbnRvIGZpbmFsCmJlc3RfaGlwZXIgPC0gc2VsZWN0X2Jlc3QoZ3JpZF9maXQsIG1ldHJpYyA9ICJybXNlIikKCmZpbmFsX21vZGVsX2ZpdCA8LSBmaW5hbGl6ZV93b3JrZmxvdygKICAgICAgICAgICAgICAgICAgICAgICAgeCA9IHdvcmtmbG93X21vZGVsYWRvLAogICAgICAgICAgICAgICAgICAgICAgICBwYXJhbWV0ZXJzID0gYmVzdF9oaXBlcgogICAgICAgICAgICAgICAgICAgICkgJT4lCiAgICAgICAgICAgICAgICAgICAgZml0KAogICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IGRhdGFfdHJhaW4KICAgICAgICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICAgICAgIHB1bGxfd29ya2Zsb3dfZml0KCkKYGBgCgojIEVzdHJhdGVnaWFzIGRlICp0dW5pbmcqCgpVbm8gZGUgbG9zIHByb2JsZW1hcyBxdWUgdGVuZW1vcyBwYXJhIGVuY29udHJhciBsb3MgbWVqb3JlcyBoaXBlcnBhcsOhbWV0cm9zIGVzIGVsIGNvc3RlIGNvbXB1dGFjaW9uYWwuICBVbmEgYWx0ZXJuYXRpdmEgZXMgbGEgYsO6c3F1ZWRhIGRlIGhpcGVycGFyw6FtZXRyb3MgY29uIG3DqXRvZG9zIGRlICpvcHRpbWl6YWNpw7NuIGJheWVzaWFuYSouIAoKRW4gdMOpcm1pbm9zIGdlbmVyYWxlcywgKmxhIG9wdGltaXphY2nDs24gYmF5ZXNpYW5hKiBkZSBoaXBlcnBhcsOhbWV0cm9zIGNvbnNpc3RlIGVuIGNyZWFyIHVuIG1vZGVsbyBwcm9iYWJpbMOtc3RpY28gZW4gZWwgcXVlIGxhIGZ1bmNpw7NuIG9iamV0aXZvIGVzIGxhIG3DqXRyaWNhIGRlIHZhbGlkYWNpw7NuIGRlbCBtb2RlbG8gKHJtc2UsIGF1YywgcHJlY2lzacOzbiwgZXRjKS4gQ29uIGVzdGEgZXN0cmF0ZWdpYSwgc2UgY29uc2lndWUgcXVlIGxhIGLDunNxdWVkYSBzZSB2YXlhIHJlZGlyaWdpZW5kbyBlbiBjYWRhIGl0ZXJhY2nDs24gaGFjaWEgbGFzIHJlZ2lvbmVzIGRlIG1heW9yIGludGVyw6lzLgoKRWwgb2JqZXRpdm8gZmluYWwgZXMgcmVkdWNpciBlbCBuw7ptZXJvIGRlIGNvbWJpbmFjaW9uZXMgZGUgaGlwZXJwYXLDoW1ldHJvcyBjb24gbGFzIHF1ZSBzZSBldmFsw7phIGVsIG1vZGVsbywgZWxpZ2llbmRvIMO6bmljYW1lbnRlIGxvcyBtZWpvcmVzIGNhbmRpZGF0b3MuIEVzdG8gc2lnbmlmaWNhIHF1ZSwgbGEgdmVudGFqYSBmcmVudGUgYSBsYXMgb3RyYXMgZXN0cmF0ZWdpYXMgbWVuY2lvbmFkYXMsIHNlIG1heGltaXphIGN1YW5kbyBlbCBlc3BhY2lvIGRlIGLDunNxdWVkYSBlcyBtdXkgYW1wbGlvIG8gbGEgZXZhbHVhY2nDs24gZGVsIG1vZGVsbyBlcyBtdXkgbGVudGEuCgpgYGB7cn0KIyBEZWZpbmljacOzbiBkZWwgbW9kZWxvIHkgbG9zIGhpcGVycGFyw6FtZXRyb3MgYSBvcHRpbWl6YXIKbW9kZWxfdHJlZSA8LSBkZWNpc2lvbl90cmVlKAogICAgICAgICAgICAgICAgIG1vZGUgICAgICAgPSAicmVncmVzc2lvbiIsCiAgICAgICAgICAgICAgICAgdHJlZV9kZXB0aCA9IHR1bmUoKSwKICAgICAgICAgICAgICAgICBtaW5fbiAgICAgID0gdHVuZSgpCiAgICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICBzZXRfZW5naW5lKGVuZ2luZSA9ICJycGFydCIpCgojIFByb2Nlc2Fkbwp0cmFuc2Zvcm1lciA8LSByZWNpcGUoCiAgICAgICAgICAgICAgICAgIGZvcm11bGEgPSBwcmljZSB+IC4sCiAgICAgICAgICAgICAgICAgIGRhdGEgPSAgZGF0YV90cmFpbgogICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICBzdGVwX25hb21pdChhbGxfcHJlZGljdG9ycygpKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9uenYoYWxsX3ByZWRpY3RvcnMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfY2VudGVyKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfc2NhbGUoYWxsX251bWVyaWMoKSwgLWFsbF9vdXRjb21lcygpKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9kdW1teShhbGxfbm9taW5hbCgpLCAtYWxsX291dGNvbWVzKCkpCgojIEVzdHJhdGVnaWEgZGUgdmFsaWRhY2nDs24geSBjcmVhY2nDs24gZGUgcGFydGljaW9uZXMKc2V0LnNlZWQoMTIzNCkKY3ZfZm9sZHMgPC0gdmZvbGRfY3YoCiAgICAgICAgICAgICAgZGF0YSAgICA9IGRhdGFfdHJhaW4sCiAgICAgICAgICAgICAgdiAgICAgICA9IDUsCiAgICAgICAgICAgICAgc3RyYXRhICA9IHByaWNlCiAgICAgICAgICAgICApCgojIHdvcmtmbG93CndvcmtmbG93X21vZGVsYWRvIDwtIHdvcmtmbG93KCkgJT4lCiAgICAgICAgICAgICAgICAgICAgIGFkZF9yZWNpcGUodHJhbnNmb3JtZXIpICU+JQogICAgICAgICAgICAgICAgICAgICBhZGRfbW9kZWwobW9kZWxfdHJlZSkKCiMgR3JpZCBkZSBoaXBlcnBhcsOhbWV0cm9zCmhpcGVycGFyX2dyaWQgPC0gZ3JpZF9yZWd1bGFyKAogICAgICAgICAgICAgICAgICAjIFJhbmdvIGRlIGLDunNxdWVkYSBwYXJhIGNhZGEgaGlwZXJwYXLDoW1ldHJvCiAgICAgICAgICAgICAgICAgIHRyZWVfZGVwdGgocmFuZ2UgPSBjKDEsIDEwKSwgdHJhbnMgPSBOVUxMKSwKICAgICAgICAgICAgICAgICAgbWluX24ocmFuZ2UgPSBjKDIsIDEwMCksIHRyYW5zID0gTlVMTCksCiAgICAgICAgICAgICAgICAgICMgTsO6bWVybyB2YWxvcmVzIHBvciBoaXBlcnBhcsOhbWV0cm8KICAgICAgICAgICAgICAgICAgbGV2ZWxzID0gYygzLCAzKQogICAgICAgICAgICAgICAgKQoKIyBPcHRpbWl6YWNpw7NuIGRlIGhpcGVycGFyw6FtZXRyb3MKcmVnaXN0ZXJEb1BhcmFsbGVsKGNvcmVzID0gcGFyYWxsZWw6OmRldGVjdENvcmVzKCkgLSAxKQoKZ3JpZF9maXQgPC0gdHVuZV9iYXllcygKICAgICAgICAgICAgICB3b3JrZmxvd19tb2RlbGFkbywgCiAgICAgICAgICAgICAgcmVzYW1wbGVzID0gY3ZfZm9sZHMsCiAgICAgICAgICAgICAgIyBJbmljaWFjacOzbiBhbGVhdG9yaWEgY29uIDIwIGNhbmRpZGF0b3MKICAgICAgICAgICAgICBpbml0aWFsID0gMjAsCiAgICAgICAgICAgICAgIyBOdW1lcm8gZGUgaXRlcmFjaW9uZXMgZGUgb3B0aW1pemFjacOzbgogICAgICAgICAgICAgIGl0ZXIgICAgPSAzMCwKICAgICAgICAgICAgICAjIE3DqXRyaWNhIG9wdGltaXphZGEKICAgICAgICAgICAgICBtZXRyaWNzID0gbWV0cmljX3NldChybXNlKSwKICAgICAgICAgICAgICBjb250cm9sID0gY29udHJvbF9iYXllcyhub19pbXByb3ZlID0gMjAsIHZlcmJvc2UgPSBGQUxTRSkKICAgICAgICAgICAgKQpzdG9wSW1wbGljaXRDbHVzdGVyKCkKYGBgCgpBbCBpZ3VhbCBxdWUgY29uIGVsIG3DqXRvZG8gYW50ZXJpb3IsIHBvZGVtb3MgdmVyIGxhIGV2b2x1Y2nDs24gZGVsIGVycm9yOgoKYGBge3J9CmF1dG9wbG90KGdyaWRfZml0LCB0eXBlID0gInBlcmZvcm1hbmNlIikgKwogIGxhYnModGl0bGUgPSAiRXZvbHVjacOzbiBkZWwgZXJyb3IiKSArCiAgdGhlbWVfYncoKQpgYGAKUGFyYSBtb3N0cmFyIGxvcyBtZWpvcmVzIGhpcGVycGFyw6FtZXRyb3MgZW5jb250cmFkb3MKCmBgYHtyfQpzaG93X2Jlc3QoeCA9IGdyaWRfZml0LCBtZXRyaWMgPSAicm1zZSIpCmBgYAoKTsOzdGVzZSBxdWUgaW5jbHVzbyBlbCAqcm1zZSogZW5jb250cmFkbyBlcyBtw6FzIGJham8gcXVlIGNvbiBlbCBjYXNvIGFudGVyaW9yLiBQb3Igw7psdGltbyByZWFsaXphbW9zIGVsIGVudHJlbmFtaWVudG8gZmluYWw6CgpgYGB7cn0KYmVzdF9oaXBlciA8LSBzZWxlY3RfYmVzdChncmlkX2ZpdCwgbWV0cmljID0gInJtc2UiKQoKZmluYWxfbW9kZWxfZml0IDwtIGZpbmFsaXplX3dvcmtmbG93KAogICAgICAgICAgICAgICAgICAgICAgICB4ID0gd29ya2Zsb3dfbW9kZWxhZG8sCiAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFtZXRlcnMgPSBiZXN0X2hpcGVyCiAgICAgICAgICAgICAgICAgICAgKSAlPiUKICAgICAgICAgICAgICAgICAgICBmaXQoCiAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gZGF0YV90cmFpbgogICAgICAgICAgICAgICAgICAgICkgJT4lCiAgICAgICAgICAgICAgICAgICAgcHVsbF93b3JrZmxvd19maXQoKQpgYGAKClkgZXZhbHVhbW9zIGxhcyBwcmVkaWNjaW9uZXMKCmBgYHtyfQpwcmVkaWNjaW9uZXMgPC0gZmluYWxfbW9kZWxfdHJlZV9maXQgICU+JQogICAgICAgICAgICAgICAgcHJlZGljdCgKICAgICAgICAgICAgICAgICAgbmV3X2RhdGEgPSBkYXRhX3Rlc3RfcHJlcCwKICAgICAgICAgICAgICAgICAgI25ld19kYXRhID0gYmFrZSh0cmFuc2Zvcm1lcl9maXQsIGRhdG9zX3Rlc3QpLAogICAgICAgICAgICAgICAgICB0eXBlID0gIm51bWVyaWMiCiAgICAgICAgICAgICAgICApCnByZWRpY2Npb25lcyAlPiUgaGVhZCgpCmBgYAoKWSBwb3Igw7psdGltbyBlbmNvbnRyYW1vcyBlbCBlcnJvciBkZSB0ZXN0OgoKCmBgYHtyfQpwcmVkaWNjaW9uZXMgPC0gcHJlZGljY2lvbmVzICU+JSAKICAgICAgICAgICAgICAgIGJpbmRfY29scyhkYXRhX3Rlc3RfcHJlcCAlPiUgc2VsZWN0KHByaWNlKSkKCnJtc2UocHJlZGljY2lvbmVzLCB0cnV0aCA9IHByaWNlLCBlc3RpbWF0ZSA9IC5wcmVkLCBuYV9ybSA9IFRSVUUpCmBgYAoKUG9kZW1vcyBsbGVnYXIgYWwgbWlzbW8gcmVzbHVsdGFkbyBhbnRlcmlvciBkZSAqKio2MTUzNioqKiBjb24gdW4gY29zdGUgY29tcHV0YWNpb25hbCBtZW5vci4gCgojIE1vZGVsb3MKCkVuIGVzdGEgc2VjY2nDs24gdmVyZW1vcyBtb2RlbG9zIGRpc3BvbmlibGVzIGVuIGxhIHBhcXVldGVyw61hIGBwYXJzbmlwYCB5IGFsIGZpbmFsIGNvbXBhcmFtb3Mgc3VzIHJlc3VsdGFkb3MsIGVzdG9zIG1vZGVsb3Mgc29uOgoKKiBbR0xNIF0oaHR0cHM6Ly9vbmxpbmUuc3RhdC5wc3UuZWR1L3N0YXQ1MDQvbGVzc29uLzYvNi4xKSAoR2VuZXJhbGl6ZWQgTGluZWFyIE1vZGVscyksCgoqIFJhbmRvbSBmb3Jlc3QsCgoqIFtTVk1dKGh0dHBzOi8vd3d3LmNpZW5jaWFkZWRhdG9zLm5ldC9kb2N1bWVudG9zLzM0X21hcXVpbmFzX2RlX3ZlY3Rvcl9zb3BvcnRlX3N1cHBvcnRfdmVjdG9yX21hY2hpbmVzKSAoU3Vwb3J0IFZlY3RvciBNYWNoaW5lKSwgeQoKKiBbTUFSU10oaHR0cDovL3VjLXIuZ2l0aHViLmlvL21hcnMpIChNdWx0aXZhcmlhdGUgYWRhcHRpdmUgcmVncmVzc2lvbiBzcGxpbmVzKS4KCiMjIEdMTQoKUmVhbGl6YXJlbW9zIGVsIG1pc21vIHByb2NlZGltaWVudG8gcXVlIGhpY2ltb3MgY29uICrDoXJib2xlcyBkZSBkZWNpc2nDs24qLCBwZXJvIGFob3JhIGNhbWJpYW5kbyBhbCBtb2RlbG8gZGUgcmVncmVzacOzbiBsaW5lYWw6CgpgYGB7cn0KIyBEZWZpbmljacOzbiBkZWwgbW9kZWxvIHkgbG9zIGhpcGVycGFyw6FtZXRyb3MgKEdMTSkKbW9kZWxfZ2xtIDwtIGxpbmVhcl9yZWcoCiAgICAgICAgICAgICAgICAgbW9kZSAgICA9ICJyZWdyZXNzaW9uIiwKICAgICAgICAgICAgICAgICBwZW5hbHR5ID0gdHVuZSgpLAogICAgICAgICAgICAgICAgIG1peHR1cmUgPSB0dW5lKCkKICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgIHNldF9lbmdpbmUoZW5naW5lID0gImdsbW5ldCIsIG5sYW1iZGEgPSAxMDApCgojIFByb2Nlc2Fkbwp0cmFuc2Zvcm1lciA8LSByZWNpcGUoCiAgICAgICAgICAgICAgICAgIGZvcm11bGEgPSBwcmljZSB+IC4sCiAgICAgICAgICAgICAgICAgIGRhdGEgPSAgZGF0YV90cmFpbgogICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICBzdGVwX25hb21pdChhbGxfcHJlZGljdG9ycygpKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9uenYoYWxsX3ByZWRpY3RvcnMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfY2VudGVyKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfc2NhbGUoYWxsX251bWVyaWMoKSwgLWFsbF9vdXRjb21lcygpKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9kdW1teShhbGxfbm9taW5hbCgpLCAtYWxsX291dGNvbWVzKCkpCgojIFZhbGlkYWNpw7NuIHkgZ2VuZXJhY2nDs24gZGUgcGFydGljaW9uZXMKc2V0LnNlZWQoMTIzNCkKY3ZfZm9sZHMgPC0gdmZvbGRfY3YoCiAgICAgICAgICAgICAgZGF0YSAgICA9IGRhdGFfdHJhaW4sCiAgICAgICAgICAgICAgdiAgICAgICA9IDUsCiAgICAgICAgICAgICAgc3RyYXRhICA9IHByaWNlCiAgICAgICAgICAgICApCgojIFdvcmtmbG93CndvcmtmbG93X21vZGVsYWRvIDwtIHdvcmtmbG93KCkgJT4lCiAgICAgICAgICAgICAgICAgICAgIGFkZF9yZWNpcGUodHJhbnNmb3JtZXIpICU+JQogICAgICAgICAgICAgICAgICAgICBhZGRfbW9kZWwobW9kZWxfZ2xtKQoKIyBHcmlkIGRlIGhpcGVycGFyw6FtZXRyb3MKaGlwZXJwYXJfZ3JpZCA8LSBncmlkX3JlZ3VsYXIoCiAgICAgICAgICAgICAgICAgICMgUmFuZ28gZGUgYsO6c3F1ZWRhIHBhcmEgY2FkYSBoaXBlcnBhcsOhbWV0cm8KICAgICAgICAgICAgICAgICAgcGVuYWx0eShyYW5nZSA9IGMoMCwgMSksIHRyYW5zID0gTlVMTCksCiAgICAgICAgICAgICAgICAgIG1peHR1cmUocmFuZ2UgPSBjKDAsIDEpLCB0cmFucyA9IE5VTEwpLAogICAgICAgICAgICAgICAgICAjIE7Dum1lcm8gZGUgY29tYmluYWNpb25lcyB0b3RhbGVzCiAgICAgICAgICAgICAgICAgIGxldmVscyA9IGMoMTAsIDEwKQogICAgICAgICAgICAgICAgKQoKIyBPcHRpbWl6YWNpw7NuIGRlIGhpcGVycGFyw6FtZXRyb3MKcmVnaXN0ZXJEb1BhcmFsbGVsKGNvcmVzID0gcGFyYWxsZWw6OmRldGVjdENvcmVzKCkgLSAxKQpncmlkX2ZpdCA8LSB0dW5lX2dyaWQoCiAgICAgICAgICAgICAgb2JqZWN0ICAgID0gd29ya2Zsb3dfbW9kZWxhZG8sCiAgICAgICAgICAgICAgcmVzYW1wbGVzID0gY3ZfZm9sZHMsCiAgICAgICAgICAgICAgbWV0cmljcyAgID0gbWV0cmljX3NldChybXNlKSwKICAgICAgICAgICAgICBjb250cm9sICAgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBUUlVFKSwKICAgICAgICAgICAgICAjIEhpcGVycGFyw6FtZXRyb3MKICAgICAgICAgICAgICBncmlkICAgICAgPSBoaXBlcnBhcl9ncmlkCiAgICAgICAgICAgICkKc3RvcEltcGxpY2l0Q2x1c3RlcigpCmBgYAoKTW9zdHJhbW9zIGxvcyBtZWpvcmVzIHJlc3VsdGFkb3MgZGUgbGEgb3B0aW1pemFjacOzbjoKCmBgYHtyfQpzaG93X2Jlc3QoZ3JpZF9maXQsIG1ldHJpYyA9ICJybXNlIiwgbiA9IDEwKQpgYGAKClVzYW1vcyBsb3MgaGlwZXJwYXLDoW1ldHJvcyDDs3B0aW1vcyBwYXJhIG9idGVuZXIgZWwgbW9kZWxvIGZpbmFsOgoKYGBge3J9CmJlc3RfaGlwZXIgPC0gc2VsZWN0X2Jlc3QoZ3JpZF9maXQsIG1ldHJpYyA9ICJybXNlIikKCm1vZGVsb19nbG0gPC0gZmluYWxpemVfd29ya2Zsb3coCiAgICAgICAgICAgICAgICB4ID0gd29ya2Zsb3dfbW9kZWxhZG8sCiAgICAgICAgICAgICAgICBwYXJhbWV0ZXJzID0gYmVzdF9oaXBlcgogICAgICAgICAgICAgICkKCm1vZGVsb19nbG1fZml0IDwtICBtb2RlbG9fZ2xtICU+JQogICAgICAgICAgICAgICAgICAgZml0KAogICAgICAgICAgICAgICAgICAgICBkYXRhID0gZGF0YV90cmFpbgogICAgICAgICAgICAgICAgICAgKQpgYGAKCkNhbGN1bGFtb3MgbGFzIHByZWRpY2Npb25lcyBjb24gZWwgdGVzdAoKYGBge3J9CnByZWRpY2Npb25lcyA8LSBtb2RlbG9fZ2xtX2ZpdCAlPiUKICAgICAgICAgICAgICAgIHByZWRpY3QoCiAgICAgICAgICAgICAgICAgIG5ld19kYXRhID0gZGF0YV90ZXN0LAogICAgICAgICAgICAgICAgICB0eXBlID0gIm51bWVyaWMiCiAgICAgICAgICAgICAgICApCmBgYAoKWSBwb3Igw7psdGltbyBjYWxjdWxhbW9zIGVsIGVycm9yCgpgYGB7cn0KcHJlZGljY2lvbmVzIDwtIHByZWRpY2Npb25lcyAlPiUgCiAgICAgICAgICAgICAgICBiaW5kX2NvbHMoZGF0YV90ZXN0X3ByZXAgJT4lIHNlbGVjdChwcmljZSkpCgplcnJvcl90ZXN0X2dsbSAgPC0gcm1zZSgKICAgICAgICAgICAgICAgICAgICAgZGF0YSAgICAgPSBwcmVkaWNjaW9uZXMsCiAgICAgICAgICAgICAgICAgICAgIHRydXRoICAgID0gcHJpY2UsCiAgICAgICAgICAgICAgICAgICAgIGVzdGltYXRlID0gLnByZWQsCiAgICAgICAgICAgICAgICAgICAgIG5hX3JtICAgID0gVFJVRQogICAgICAgICAgICAgICAgICAgKSAlPiUKICAgICAgICAgICAgICAgICAgIG11dGF0ZSgKICAgICAgICAgICAgICAgICAgICAgbW9kZWxvID0gIkdMTSIKICAgICAgICAgICAgICAgICAgICkKZXJyb3JfdGVzdF9nbG0KYGBgCgojIyBSYW5kb20gRm9yZXN0CgpVbiBtb2RlbG8gWypSYW5kb20gRm9yZXN0Kl0oaHR0cHM6Ly93d3cuY2llbmNpYWRlZGF0b3MubmV0L2RvY3VtZW50b3MvMzNfYXJib2xlc19kZV9wcmVkaWNjaW9uX2JhZ2dpbmdfcmFuZG9tX2ZvcmVzdF9ib29zdGluZyNSYW5kb21fRm9yZXN0KSBlc3TDoSBmb3JtYWRvIHBvciB1biBjb25qdW50byBkZSDDoXJib2xlcyBkZSBkZWNpc2nDs24gaW5kaXZpZHVhbGVzLCBjYWRhIHVubyBhanVzdGFkbyBlbXBsZWFuZG8gdW5hIG11ZXN0cmEgWypib290c3RyYXBwaW5nKl0oaHR0cHM6Ly93d3cuY2llbmNpYWRlZGF0b3MubmV0L2RvY3VtZW50b3MvMzBfY3Jvc3MtdmFsaWRhdGlvbl9vbmVsZWF2ZW91dF9ib290c3RyYXApIGRlIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvLgoKRW4gZWwgZW50cmVuYW1pZW50byBkZSBjYWRhIMOhcmJvbCwgbGFzIG9ic2VydmFjaW9uZXMgc2UgdmFuIGRpc3RyaWJ1eWVuZG8gcG9yIG5vZG9zIGdlbmVyYW5kbyBsYSBlc3RydWN0dXJhIGRlbCDDoXJib2wgaGFzdGEgYWxjYW56YXIgdW4gbm9kbyB0ZXJtaW5hbC4gQ3VhbmRvIHNlIHF1aWVyZSBwcmVkZWNpciB1bmEgbnVldmEgb2JzZXJ2YWNpw7NuLCBlc3RhIHJlY29ycmUgZWwgw6FyYm9sIGFjb3JkZSBhbCB2YWxvciBkZSBzdXMgcHJlZGljdG9yZXMgaGFzdGEgYWxjYW56YXIgdW5vIGRlIGxvcyBub2RvcyB0ZXJtaW5hbGVzLiBMYSBwcmVkaWNjacOzbiBkZWwgw6FyYm9sIGVzIGxhIG1lZGlhIGRlIGxhIHZhcmlhYmxlIHJlc3B1ZXN0YSAobGEgbW9kYSBlbiBwcm9ibGVtYXMgZGUgY2xhc2lmaWNhY2nDs24pIGRlIHRvZGFzIGxhcyBvYnNlcnZhY2lvbmVzIGRlIGVudHJlbmFtaWVudG8gcXVlIGVzdMOhbiBlbiBlc3RlIG1pc21vIG5vZG8gdGVybWluYWwuIExhIHByZWRpY2Npw7NuIGRlIHVuIG1vZGVsbyBSYW5kb20gRm9yZXN0IGVzIGxhIG1lZGlhIGRlIGxhcyBwcmVkaWNjaW9uZXMgZGUgdG9kb3MgbG9zIMOhcmJvbGVzIHF1ZSBsbyBmb3JtYW4uCgpFbmNvbnRyZW1vcyBlbCBtb2RlbG8gdXRpbGl6YW5kbyAqcmFuZG9tIGZvcmVzdCo6CgpgYGB7cn0KIyBEZWZpbmljacOzbiBkZWwgbW9kZWxvIHkgbG9zIGhpcGVycGFyw6FtZXRyb3MgYSBvcHRpbWl6YXIKbW9kZWxvX3JmIDwtIHJhbmRfZm9yZXN0KAogICAgICAgICAgICAgICAgIG1vZGUgID0gInJlZ3Jlc3Npb24iLAogICAgICAgICAgICAgICAgIG10cnkgID0gdHVuZSgpLAogICAgICAgICAgICAgICAgIHRyZWVzID0gdHVuZSgpLAogICAgICAgICAgICAgICAgIG1pbl9uID0gdHVuZSgpCiAgICAgICAgICAgICAgKSAlPiUKICAgICAgICAgICAgICBzZXRfZW5naW5lKGVuZ2luZSA9ICJyYW5nZXIiKQoKIyBQcm9jZXNhZG8KdHJhbnNmb3JtZXIgPC0gcmVjaXBlKAogICAgICAgICAgICAgICAgICBmb3JtdWxhID0gcHJpY2UgfiAuLAogICAgICAgICAgICAgICAgICBkYXRhID0gIGRhdGFfdHJhaW4KICAgICAgICAgICAgICAgKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9uYW9taXQoYWxsX3ByZWRpY3RvcnMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfbnp2KGFsbF9wcmVkaWN0b3JzKCkpICU+JQogICAgICAgICAgICAgICBzdGVwX2NlbnRlcihhbGxfbnVtZXJpYygpLCAtYWxsX291dGNvbWVzKCkpICU+JQogICAgICAgICAgICAgICBzdGVwX3NjYWxlKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfZHVtbXkoYWxsX25vbWluYWwoKSwgLWFsbF9vdXRjb21lcygpKQoKIyBFc3RyYXRlZ2lhIGRlIHZhbGlkYWNpw7NuIHkgY3JlYWNpw7NuIGRlIHBhcnRpY2lvbmVzCnNldC5zZWVkKDEyMzQpCmN2X2ZvbGRzIDwtIHZmb2xkX2N2KAogICAgICAgICAgICAgIGRhdGEgICAgPSBkYXRhX3RyYWluLAogICAgICAgICAgICAgIHYgICAgICAgPSA1LAogICAgICAgICAgICAgIHN0cmF0YSAgPSBwcmljZQogICAgICAgICAgICAgKQoKIyBXb3JrZmxvdwp3b3JrZmxvd19tb2RlbGFkbyA8LSB3b3JrZmxvdygpICU+JQogICAgICAgICAgICAgICAgICAgICBhZGRfcmVjaXBlKHRyYW5zZm9ybWVyKSAlPiUKICAgICAgICAgICAgICAgICAgICAgYWRkX21vZGVsKG1vZGVsb19yZikKCiMgR3JpZCBkZSBoaXBlcnBhcsOhbWV0cm9zCmhpcGVycGFyX2dyaWQgPC0gZ3JpZF9tYXhfZW50cm9weSgKICAgICAgICAgICAgICAgICAgIyBSYW5nbyBkZSBiw7pzcXVlZGEgcGFyYSBjYWRhIGhpcGVycGFyw6FtZXRybwogICAgICAgICAgICAgICAgICBtdHJ5KHJhbmdlID0gYygxTCwgMTBMKSwgdHJhbnMgPSBOVUxMKSwKICAgICAgICAgICAgICAgICAgdHJlZXMocmFuZ2UgPSBjKDUwMEwsIDMwMDBMKSwgdHJhbnMgPSBOVUxMKSwKICAgICAgICAgICAgICAgICAgbWluX24ocmFuZ2UgPSBjKDJMLCAxMDBMKSwgdHJhbnMgPSBOVUxMKSwKICAgICAgICAgICAgICAgICAgIyBOw7ptZXJvIGRlIGNvbWJpbmFjaW9uZXMgdG90YWxlcwogICAgICAgICAgICAgICAgICBzaXplID0gMTAwCiAgICAgICAgICAgICAgICApCgojIE9wdGltaXphY2nDs24gZGUgaGlwZXJwYXLDoW1ldHJvcwpyZWdpc3RlckRvUGFyYWxsZWwoY29yZXMgPSBwYXJhbGxlbDo6ZGV0ZWN0Q29yZXMoKSAtIDEpCmdyaWRfZml0IDwtIHR1bmVfZ3JpZCgKICAgICAgICAgICAgICBvYmplY3QgICAgPSB3b3JrZmxvd19tb2RlbGFkbywKICAgICAgICAgICAgICByZXNhbXBsZXMgPSBjdl9mb2xkcywKICAgICAgICAgICAgICBtZXRyaWNzICAgPSBtZXRyaWNfc2V0KHJtc2UpLAogICAgICAgICAgICAgIGNvbnRyb2wgICA9IGNvbnRyb2xfcmVzYW1wbGVzKHNhdmVfcHJlZCA9IFRSVUUpLAogICAgICAgICAgICAgICMgSGlwZXJwYXLDoW1ldHJvcwogICAgICAgICAgICAgIGdyaWQgICAgICA9IGhpcGVycGFyX2dyaWQKICAgICAgICAgICAgKQpzdG9wSW1wbGljaXRDbHVzdGVyKCkKYGBgCgpNb3N0cmFtb3MgbG9zIHJlc3VsdGFkb3MgZGUgb3B0aW1pemFjacOzbjoKCmBgYHtyfQpzaG93X2Jlc3QoZ3JpZF9maXQsIG1ldHJpYyA9ICJybXNlIikKYGBgCgpVc2Ftb3MgbG9zIG1lam9yZXMgaGlwZXJwYXLDoW1ldHJvcyBlbiBlbCBtb2RlbG86CgpgYGB7cn0KYmVzdF9oaXBlciA8LSBzZWxlY3RfYmVzdChncmlkX2ZpdCwgbWV0cmljID0gInJtc2UiKQoKbW9kZWxvX3JmIDwtIGZpbmFsaXplX3dvcmtmbG93KAogICAgICAgICAgICAgICAgeCA9IHdvcmtmbG93X21vZGVsYWRvLAogICAgICAgICAgICAgICAgcGFyYW1ldGVycyA9IGJlc3RfaGlwZXIKICAgICAgICAgICAgICkKCm1vZGVsb19yZl9maXQgPC0gbW9kZWxvX3JmICU+JQogICAgICAgICAgICAgICAgIGZpdCgKICAgICAgICAgICAgICAgICAgZGF0YSA9IGRhdGFfdHJhaW4KICAgICAgICAgICAgICAgICApCmBgYAoKRXZhbHVhbW9zIGxhcyBwcmVkaWNjaW9uZXMgZGUgdGVzdAoKYGBge3J9CnByZWRpY2Npb25lcyA8LSBtb2RlbG9fcmZfZml0ICU+JQogICAgICAgICAgICAgICAgcHJlZGljdCgKICAgICAgICAgICAgICAgICAgbmV3X2RhdGEgPSBkYXRhX3Rlc3QsCiAgICAgICAgICAgICAgICAgIHR5cGUgICAgID0gIm51bWVyaWMiCiAgICAgICAgICAgICAgICApCmBgYAoKeSBlbmNvbnRyYW1vcyBlbCBlcnJvcgoKYGBge3J9CnByZWRpY2Npb25lcyA8LSBwcmVkaWNjaW9uZXMgJT4lIAogICAgICAgICAgICAgICAgYmluZF9jb2xzKGRhdGFfdGVzdF9wcmVwICU+JSBzZWxlY3QocHJpY2UpKQoKZXJyb3JfdGVzdF9yZiAgPC0gcm1zZSgKICAgICAgICAgICAgICAgICAgICAgZGF0YSAgICAgPSBwcmVkaWNjaW9uZXMsCiAgICAgICAgICAgICAgICAgICAgIHRydXRoICAgID0gcHJpY2UsCiAgICAgICAgICAgICAgICAgICAgIGVzdGltYXRlID0gLnByZWQsCiAgICAgICAgICAgICAgICAgICAgIG5hX3JtICAgID0gVFJVRQogICAgICAgICAgICAgICAgICAgKSAlPiUKICAgICAgICAgICAgICAgICAgIG11dGF0ZSgKICAgICAgICAgICAgICAgICAgICAgbW9kZWxvID0gIlJGIgogICAgICAgICAgICAgICAgICAgKQplcnJvcl90ZXN0X3JmCmBgYAoKIyMgU1ZNCgpFbCBtw6l0b2RvIGRlIGNsYXNpZmljYWNpw7NuLXJlZ3Jlc2nDs24gW1N1cG9ydCBWZWN0b3IgTWFjaGluZV0oaHR0cHM6Ly93d3cuY2llbmNpYWRlZGF0b3MubmV0L2RvY3VtZW50b3MvMzRfbWFxdWluYXNfZGVfdmVjdG9yX3NvcG9ydGVfc3VwcG9ydF92ZWN0b3JfbWFjaGluZXMpIChTVk0pIGVzIHVuIG3DqXRvZG8gZGUgbm8gcGFyYW3DqXRyaWNvLCBzZSBmdW5kYW1lbnRhIGVuIGVsIGNvbmNlcHRvIGRlIGhpcGVycGxhbm8geSBkZSBtw6F4aW1hIHNlcGFyYWNpw7NuLiAgTG9zIG1vZGVsb3MgU1ZNIGVuY3VlbnRyYW4gZWwgaGlwZXJwbGFubyDDs3B0aW1vIGNhcGF6IGRlIHByZWRlY2lyIGVsIHZhbG9yL2NsYXNpZmljYWNpw7NuIGRlIGxhcyBvYnNlcnZhY2lvbmVzIGNvbiBlbCBtZW5vciBlcnJvciBwb3NpYmxlLgoKUGFyYSBxdWUgZWwgbW9kZWxvIG5vcyBmdW5jaW9uZSBjb3JyZWN0YW1lbnRlIGRlYmVtb3MgaW5zdGFsYXIgZWwgcGFxdWV0ZSBga2VybmxhYmAuCgoKYGBge3J9CiMgRGVmaW5pY2nDs24gZGVsIG1vZGVsbyB5IGxvcyBoaXBlcnBhcsOhbWV0cm9zIGEgb3B0aW1pemFyCm1vZGVsb19zdm0gPC0gc3ZtX3JiZigKICAgICAgICAgICAgICAgICBtb2RlICAgICAgPSAicmVncmVzc2lvbiIsCiAgICAgICAgICAgICAgICAgY29zdCAgICAgID0gdHVuZSgpLAogICAgICAgICAgICAgICAgIHJiZl9zaWdtYSA9IHR1bmUoKSwKICAgICAgICAgICAgICAgICBtYXJnaW4gICAgPSB0dW5lKCkKICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgIHNldF9lbmdpbmUoZW5naW5lID0gImtlcm5sYWIiKQoKIyBQcm9jZXNhZG8KdHJhbnNmb3JtZXIgPC0gcmVjaXBlKAogICAgICAgICAgICAgICAgICBmb3JtdWxhID0gcHJpY2UgfiAuLAogICAgICAgICAgICAgICAgICBkYXRhID0gIGRhdGFfdHJhaW4KICAgICAgICAgICAgICAgKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9uYW9taXQoYWxsX3ByZWRpY3RvcnMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfbnp2KGFsbF9wcmVkaWN0b3JzKCkpICU+JQogICAgICAgICAgICAgICBzdGVwX2NlbnRlcihhbGxfbnVtZXJpYygpLCAtYWxsX291dGNvbWVzKCkpICU+JQogICAgICAgICAgICAgICBzdGVwX3NjYWxlKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfZHVtbXkoYWxsX25vbWluYWwoKSwgLWFsbF9vdXRjb21lcygpKQoKIyBFc3RyYXRlZ2lhIGRlIHZhbGlkYWNpw7NuIHkgY3JlYWNpw7NuIGRlIHBhcnRpY2lvbmVzCnNldC5zZWVkKDEyMzQpCmN2X2ZvbGRzIDwtIHZmb2xkX2N2KAogICAgICAgICAgICAgIGRhdGEgICAgPSBkYXRhX3RyYWluLAogICAgICAgICAgICAgIHYgICAgICAgPSA1LAogICAgICAgICAgICAgIHN0cmF0YSAgPSBwcmljZQogICAgICAgICAgICAgKQoKIyBXb3JrZmxvdwp3b3JrZmxvd19tb2RlbGFkbyA8LSB3b3JrZmxvdygpICU+JQogICAgICAgICAgICAgICAgICAgICBhZGRfcmVjaXBlKHRyYW5zZm9ybWVyKSAlPiUKICAgICAgICAgICAgICAgICAgICAgYWRkX21vZGVsKG1vZGVsb19zdm0pCgojIEdyaWQgaGlwZXJwYXLDoW1ldHJvcwpoaXBlcnBhcl9ncmlkIDwtIGdyaWRfcmFuZG9tKAogICAgICAgICAgICAgICAgICAjIFJhbmdvIGRlIGLDunNxdWVkYSBwYXJhIGNhZGEgaGlwZXJwYXLDoW1ldHJvCiAgICAgICAgICAgICAgICAgIGNvc3QocmFuZ2UgPSBjKC0xMCwgLTEpLCB0cmFucyA9IGxvZzJfdHJhbnMoKSksCiAgICAgICAgICAgICAgICAgIHJiZl9zaWdtYShyYW5nZSA9IGMoLTEwLCAwKSwgdHJhbnMgPSBsb2cxMF90cmFucygpKSwKICAgICAgICAgICAgICAgICAgc3ZtX21hcmdpbihyYW5nZSA9IGMoMCwgMC4yKSwgdHJhbnMgPSBOVUxMKSwgCiAgICAgICAgICAgICAgICAgICMgTsO6bWVybyBkZSBjb21iaW5hY2lvbmVzIHRvdGFsZXMKICAgICAgICAgICAgICAgICAgc2l6ZSA9IDEwMAogICAgICAgICAgICAgICAgKQoKIyBPcHRpbWl6YWNpw7NuIGRlIGhpcGVycGFyw6FtZXRyb3MKcmVnaXN0ZXJEb1BhcmFsbGVsKGNvcmVzID0gcGFyYWxsZWw6OmRldGVjdENvcmVzKCkgLSAxKQpncmlkX2ZpdCA8LSB0dW5lX2dyaWQoCiAgICAgICAgICAgICAgb2JqZWN0ICAgID0gd29ya2Zsb3dfbW9kZWxhZG8sCiAgICAgICAgICAgICAgcmVzYW1wbGVzID0gY3ZfZm9sZHMsCiAgICAgICAgICAgICAgbWV0cmljcyAgID0gbWV0cmljX3NldChybXNlKSwKICAgICAgICAgICAgICBjb250cm9sICAgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBUUlVFKSwKICAgICAgICAgICAgICAjIEhpcGVycGFyw6FtZXRyb3MKICAgICAgICAgICAgICBncmlkICAgICAgPSBoaXBlcnBhcl9ncmlkCiAgICAgICAgICAgICkKc3RvcEltcGxpY2l0Q2x1c3RlcigpCmBgYAoKTW9zdHJhbW9zIGxvcyByZXN1bHRhZG9zIGRlIGxhIG9wdGltaXphY2nDs24KYGBge3J9CnNob3dfYmVzdChncmlkX2ZpdCwgbWV0cmljID0gInJtc2UiLCBuID0gMTApCmBgYAoKWSB1dGlsaXphbW9zIGxvcyBoaXBlcnBhcsOhbWV0cm9zIMOzcHRpbW9zIHBhcmEgZWwgbW9kZWxvCmBgYHtyfQpiZXN0X2hpcGVyIDwtIHNlbGVjdF9iZXN0KGdyaWRfZml0LCBtZXRyaWMgPSAicm1zZSIpCgptb2RlbG9fc3ZtIDwtIGZpbmFsaXplX3dvcmtmbG93KAogICAgICAgICAgICAgICAgICB4ID0gd29ya2Zsb3dfbW9kZWxhZG8sCiAgICAgICAgICAgICAgICAgIHBhcmFtZXRlcnMgPSBiZXN0X2hpcGVyCiAgICAgICAgICAgICAgKQoKbW9kZWxvX3N2bV9maXQgPC0gbW9kZWxvX3N2bSAlPiUKICAgICAgICAgICAgICAgICAgZml0KAogICAgICAgICAgICAgICAgICAgIGRhdGEgPSBkYXRhX3RyYWluCiAgICAgICAgICAgICAgICAgICkKYGBgCgpFbmNvbnRyYW1vcyBsYXMgcHJlZGljY2lvbmVzIGVuIGVsIHRlc3Q6CmBgYHtyfQpwcmVkaWNjaW9uZXMgPC0gbW9kZWxvX3N2bV9maXQgJT4lCiAgICAgICAgICAgICAgICBwcmVkaWN0KAogICAgICAgICAgICAgICAgICBuZXdfZGF0YSA9IGRhdGFfdGVzdCwKICAgICAgICAgICAgICAgICAgdHlwZSAgICAgPSAibnVtZXJpYyIKICAgICAgICAgICAgICAgICkKYGBgCgpZIGZpbmFsbWVudGUgZW5jb250cmFtb3MgZWwgZXJyb3IKCmBgYHtyfQpwcmVkaWNjaW9uZXMgPC0gcHJlZGljY2lvbmVzICU+JSAKICAgICAgICAgICAgICAgIGJpbmRfY29scyhkYXRhX3Rlc3RfcHJlcCAlPiUgc2VsZWN0KHByaWNlKSkKCmVycm9yX3Rlc3Rfc3ZtIDwtIHJtc2UoCiAgICAgICAgICAgICAgICAgICAgIGRhdGEgICAgID0gcHJlZGljY2lvbmVzLAogICAgICAgICAgICAgICAgICAgICB0cnV0aCAgICA9IHByaWNlLAogICAgICAgICAgICAgICAgICAgICBlc3RpbWF0ZSA9IC5wcmVkLAogICAgICAgICAgICAgICAgICAgICBuYV9ybSAgICA9IFRSVUUKICAgICAgICAgICAgICAgICAgICkgJT4lCiAgICAgICAgICAgICAgICAgICBtdXRhdGUoCiAgICAgICAgICAgICAgICAgICAgIG1vZGVsbyA9ICJTVk0iCiAgICAgICAgICAgICAgICAgICApCmVycm9yX3Rlc3Rfc3ZtCmBgYAoKIyMgTUFSUwoKW011bHRpdmFyaWF0ZSBhZGFwdGl2ZSByZWdyZXNzaW9uIHNwbGluZXNdKGh0dHA6Ly91Yy1yLmdpdGh1Yi5pby9tYXJzKSAoTUFSUykgZXMgdW5hIGV4dGVuc2nDs24gbm8gcGFyYW3DqXRyaWNhIGRlIGxvcyBtb2RlbG9zIGRlIHJlZ3Jlc2nDs24gbGluZWFsIHF1ZSBhdXRvbWF0aXphIGxhIGluY29ycG9yYWNpw7NuIGRlIHJlbGFjaW9uZXMgbm8gbGluZWFsZXMgZW50cmUgbG9zIHByZWRpY3RvcmVzIHkgbGEgdmFyaWFibGUgcmVzcHVlc3RhLCBhc8OtIGNvbW8gaW50ZXJhY2Npb25lcyBlbnRyZSBsb3MgcHJlZGljdG9yZXMuCgpQYXJhIHF1ZSBlc3RlIGZ1bmNpb25lIGJpZW4gZGViZW1vcyBpbnN0YWxhciBlbCBwYXF1ZXRlIGBlYXJ0aGAuCgpgYGB7cn0KIyBEZWZpbmljacOzbiBkZWwgbW9kZWxvIHkgbG9zIGhpcGVycGFyw6FtZXRyb3MgYSBvcHRpbWl6YXIKbW9kZWxvX21hcnMgPC0gbWFycygKICAgICAgICAgICAgICAgICBtb2RlICA9ICJyZWdyZXNzaW9uIiwKICAgICAgICAgICAgICAgICBudW1fdGVybXMgPSB0dW5lKCksCiAgICAgICAgICAgICAgICAgcHJvZF9kZWdyZWUgPSAyLAogICAgICAgICAgICAgICAgIHBydW5lX21ldGhvZCA9ICJub25lIgogICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICBzZXRfZW5naW5lKGVuZ2luZSA9ICJlYXJ0aCIpCgojIFByb2Nlc2Fkbwp0cmFuc2Zvcm1lciA8LSByZWNpcGUoCiAgICAgICAgICAgICAgICAgIGZvcm11bGEgPSBwcmljZSB+IC4sCiAgICAgICAgICAgICAgICAgIGRhdGEgPSAgZGF0YV90cmFpbgogICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICBzdGVwX25hb21pdChhbGxfcHJlZGljdG9ycygpKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9uenYoYWxsX3ByZWRpY3RvcnMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfY2VudGVyKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lCiAgICAgICAgICAgICAgIHN0ZXBfc2NhbGUoYWxsX251bWVyaWMoKSwgLWFsbF9vdXRjb21lcygpKSAlPiUKICAgICAgICAgICAgICAgc3RlcF9kdW1teShhbGxfbm9taW5hbCgpLCAtYWxsX291dGNvbWVzKCkpCgojIFZhbGlkYWNpw7NuIHkgY3JlYWNpw7NuIGRlIHBhcnRpY2lvbmVzCnNldC5zZWVkKDEyMzQpCmN2X2ZvbGRzIDwtIHZmb2xkX2N2KAogICAgICAgICAgICAgIGRhdGEgICAgPSBkYXRhX3RyYWluLAogICAgICAgICAgICAgIHYgICAgICAgPSA1LAogICAgICAgICAgICAgIHN0cmF0YSAgPSBwcmljZQogICAgICAgICAgICAgKQoKIyBXb3JrZmxvdwp3b3JrZmxvd19tb2RlbGFkbyA8LSB3b3JrZmxvdygpICU+JQogICAgICAgICAgICAgICAgICAgICBhZGRfcmVjaXBlKHRyYW5zZm9ybWVyKSAlPiUKICAgICAgICAgICAgICAgICAgICAgYWRkX21vZGVsKG1vZGVsb19tYXJzKQoKIyBHcmlkIGRlIGhpcGVycGFyw6FtZXRyb3MKaGlwZXJwYXJfZ3JpZCA8LSBncmlkX3JlZ3VsYXIoCiAgICAgICAgICAgICAgICAgICMgUmFuZ28gZGUgYsO6c3F1ZWRhIHBhcmEgY2FkYSBoaXBlcnBhcsOhbWV0cm8KICAgICAgICAgICAgICAgICAgbnVtX3Rlcm1zKHJhbmdlID0gYygxLCAyMCksIHRyYW5zID0gTlVMTCksCiAgICAgICAgICAgICAgICAgIGxldmVscyA9IDIwCiAgICAgICAgICAgICAgICApCgojIE9wdGltaXphY2nDs24gZGUgaGlwZXJwYXLDoW1ldHJvcwpyZWdpc3RlckRvUGFyYWxsZWwoY29yZXMgPSBwYXJhbGxlbDo6ZGV0ZWN0Q29yZXMoKSAtIDEpCmdyaWRfZml0IDwtIHR1bmVfZ3JpZCgKICAgICAgICAgICAgICBvYmplY3QgICAgPSB3b3JrZmxvd19tb2RlbGFkbywKICAgICAgICAgICAgICByZXNhbXBsZXMgPSBjdl9mb2xkcywKICAgICAgICAgICAgICBtZXRyaWNzICAgPSBtZXRyaWNfc2V0KHJtc2UpLAogICAgICAgICAgICAgIGNvbnRyb2wgICA9IGNvbnRyb2xfcmVzYW1wbGVzKHNhdmVfcHJlZCA9IFRSVUUpLAogICAgICAgICAgICAgICMgSGlwZXJwYXLDoW1ldHJvcwogICAgICAgICAgICAgIGdyaWQgICAgICA9IGhpcGVycGFyX2dyaWQKICAgICAgICAgICAgKQpzdG9wSW1wbGljaXRDbHVzdGVyKCkKYGBgCgpNb3N0cmFtb3MgbG9zIHJlc3VsdGFkb3MgZGUgbGEgb3B0aW1pemFjacOzbgoKYGBge3J9CnNob3dfYmVzdChncmlkX2ZpdCwgbWV0cmljID0gInJtc2UiLCBuID0gMTApCmBgYAoKVXRpbGl6YW1vcyBlbiBlbCBtb2RlbG8gbG9zIGhpcGVycGFyw6FtZXRyb3Mgw7NwdGltb3M6CmBgYHtyfQpiZXN0X2hpcGVyIDwtIHNlbGVjdF9iZXN0KGdyaWRfZml0LCBtZXRyaWMgPSAicm1zZSIpCgptb2RlbG9fbWFycyA8LSBmaW5hbGl6ZV93b3JrZmxvdygKICAgICAgICAgICAgICAgICAgeCA9IHdvcmtmbG93X21vZGVsYWRvLAogICAgICAgICAgICAgICAgICBwYXJhbWV0ZXJzID0gYmVzdF9oaXBlcgogICAgICAgICAgICAgICApCgptbW9kZWxvX21hcnNfZml0IDwtIG1vZGVsb19zdm0gJT4lCiAgICAgICAgICAgICAgICAgICAgZml0KAogICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IGRhdGFfdHJhaW4KICAgICAgICAgICAgICAgICAgICApCmBgYAoKRW5jb250cmFtb3MgbGFzIHByZWRpY2Npb25lcyB0ZXN0OgoKYGBge3J9CnByZWRpY2Npb25lcyA8LSBtbW9kZWxvX21hcnNfZml0ICU+JQogICAgICAgICAgICAgICAgcHJlZGljdCgKICAgICAgICAgICAgICAgICAgbmV3X2RhdGEgPSBkYXRhX3Rlc3QsCiAgICAgICAgICAgICAgICAgIHR5cGUgICAgID0gIm51bWVyaWMiCiAgICAgICAgICAgICAgICApCmBgYAoKRW5jb250cmFtb3MgZWwgZXJyb3IKCmBgYHtyfQpwcmVkaWNjaW9uZXMgPC0gcHJlZGljY2lvbmVzICU+JSAKICAgICAgICAgICAgICAgIGJpbmRfY29scyhkYXRhX3Rlc3RfcHJlcCAlPiUgc2VsZWN0KHByaWNlKSkKCmVycm9yX3Rlc3RfbWFycyA8LSBybXNlKAogICAgICAgICAgICAgICAgICAgICAgZGF0YSAgICAgPSBwcmVkaWNjaW9uZXMsCiAgICAgICAgICAgICAgICAgICAgICB0cnV0aCAgICA9IHByaWNlLAogICAgICAgICAgICAgICAgICAgICAgZXN0aW1hdGUgPSAucHJlZCwKICAgICAgICAgICAgICAgICAgICAgIG5hX3JtICAgID0gVFJVRQogICAgICAgICAgICAgICAgICAgKSAlPiUKICAgICAgICAgICAgICAgICAgICBtdXRhdGUoCiAgICAgICAgICAgICAgICAgICAgICBtb2RlbG8gPSAiTUFSUyIKICAgICAgICAgICAgICAgICAgICApCmVycm9yX3Rlc3RfbWFycwpgYGAKCiMjIENvbXBhcmFjacOzbiAKCiMjIyBFcnJvciBkZSB2YWxpZGFjacOzbiBjcnV6YWRhCgpgYGB7cn0Kc2V0LnNlZWQoMTIzNCkKY3ZfZm9sZHMgPC0gdmZvbGRfY3YoCiAgICAgICAgICAgICAgZGF0YSAgICA9IGRhdGFfdHJhaW4sCiAgICAgICAgICAgICAgdiAgICAgICA9IDUsCiAgICAgICAgICAgICAgcmVwZWF0cyA9IDUsCiAgICAgICAgICAgICAgc3RyYXRhICA9IHByaWNlCiAgICAgICAgICAgICkKCgpyZWdpc3RlckRvUGFyYWxsZWwoY29yZXMgPSBwYXJhbGxlbDo6ZGV0ZWN0Q29yZXMoKSAtIDEpCgp2YWxpZGFjaW9uX2dsbSA8LSBmaXRfcmVzYW1wbGVzKAogICAgICAgICAgICAgICAgICAgIG9iamVjdCAgICAgICA9IG1vZGVsb19nbG0sCiAgICAgICAgICAgICAgICAgICAgIyBwcmVwcm9jZXNzb3IgPSB0cmFuc2Zvcm1lciwKICAgICAgICAgICAgICAgICAgICByZXNhbXBsZXMgICAgPSBjdl9mb2xkcywKICAgICAgICAgICAgICAgICAgICBtZXRyaWNzICAgICAgPSBtZXRyaWNfc2V0KHJtc2UpLAogICAgICAgICAgICAgICAgICAgIGNvbnRyb2wgICAgICA9IGNvbnRyb2xfcmVzYW1wbGVzKHNhdmVfcHJlZCA9IEZBTFNFKQogICAgICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICAgICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXplID0gRkFMU0UpICU+JQogICAgICAgICAgICAgICAgICBtdXRhdGUobW9kZWxvID0gIkdMTSIpCgp2YWxpZGFjaW9uX3N2bSA8LSBmaXRfcmVzYW1wbGVzKAogICAgICAgICAgICAgICAgICAgIG9iamVjdCAgICAgICAgPSBtb2RlbG9fc3ZtLAogICAgICAgICAgICAgICAgICAgICNwcmVwcm9jZXNzb3IgPSB0cmFuc2Zvcm1lciwKICAgICAgICAgICAgICAgICAgICByZXNhbXBsZXMgICAgID0gY3ZfZm9sZHMsCiAgICAgICAgICAgICAgICAgICAgbWV0cmljcyAgICAgICA9IG1ldHJpY19zZXQocm1zZSksCiAgICAgICAgICAgICAgICAgICAgY29udHJvbCAgICAgICA9IGNvbnRyb2xfcmVzYW1wbGVzKHNhdmVfcHJlZCA9IEZBTFNFKQogICAgICAgICAgICAgICAgICApICU+JQogICAgICAgICAgICAgICAgICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXplID0gRkFMU0UpICU+JQogICAgICAgICAgICAgICAgICBtdXRhdGUobW9kZWxvID0gIlNWTSIpCgp2YWxpZGFjaW9uX3JmIDwtIGZpdF9yZXNhbXBsZXMoCiAgICAgICAgICAgICAgICAgICAgb2JqZWN0ICAgICAgID0gbW9kZWxvX3JmLAogICAgICAgICAgICAgICAgICAgICMgcHJlcHJvY2Vzc29yID0gdHJhbnNmb3JtZXIsCiAgICAgICAgICAgICAgICAgICAgcmVzYW1wbGVzICAgID0gY3ZfZm9sZHMsCiAgICAgICAgICAgICAgICAgICAgbWV0cmljcyAgICAgID0gbWV0cmljX3NldChybXNlKSwKICAgICAgICAgICAgICAgICAgICBjb250cm9sICAgICAgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBGQUxTRSkKICAgICAgICAgICAgICAgICAgKSAlPiUKICAgICAgICAgICAgICAgICAgY29sbGVjdF9tZXRyaWNzKHN1bW1hcml6ZSA9IEZBTFNFKSAlPiUKICAgICAgICAgICAgICAgICAgbXV0YXRlKG1vZGVsbyA9ICJSRiIpCgp2YWxpZGFjaW9uX21hcnMgPC0gZml0X3Jlc2FtcGxlcygKICAgICAgICAgICAgICAgICAgICAgb2JqZWN0ICAgICAgID0gbW9kZWxvX21hcnMsCiAgICAgICAgICAgICAgICAgICAgICMgcHJlcHJvY2Vzc29yID0gdHJhbnNmb3JtZXIsCiAgICAgICAgICAgICAgICAgICAgIHJlc2FtcGxlcyAgICA9IGN2X2ZvbGRzLAogICAgICAgICAgICAgICAgICAgICBtZXRyaWNzICAgICAgPSBtZXRyaWNfc2V0KHJtc2UpLAogICAgICAgICAgICAgICAgICAgICBjb250cm9sICAgICAgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBGQUxTRSkKICAgICAgICAgICAgICAgICAgICkgJT4lCiAgICAgICAgICAgICAgICAgICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXplID0gRkFMU0UpICU+JQogICAgICAgICAgICAgICAgICAgbXV0YXRlKG1vZGVsbyA9ICJNQVJTIikKCnN0b3BJbXBsaWNpdENsdXN0ZXIoKQpgYGAKCmBgYHtyfQpiaW5kX3Jvd3MoCiAgdmFsaWRhY2lvbl9nbG0sCiAgdmFsaWRhY2lvbl9yZiwKICB2YWxpZGFjaW9uX3N2bSwKICB2YWxpZGFjaW9uX21hcnMKKSAlPiUKZ2dwbG90KGFlcyh4ID0gbW9kZWxvLCB5ID0gLmVzdGltYXRlLCBjb2xvciA9IG1vZGVsbykpICsKICBnZW9tX3Zpb2xpbigpICsKICBnZW9tX2JveHBsb3Qob3V0bGllci5zaGFwZSA9IE5BLCB3aWR0aCA9IDAuMikgKwogIGdlb21fcG9pbnQoYWxwSGEgPSAwLjEpICsKICBsYWJzKHRpdGxlID0gIkVycm9yIGRlIHZhbGlkYWNpw7NuIGNydXphZGEiLCB5ID0gIlJNU0UiKSArCiAgdGhlbWVfYncoKSArCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKQpgYGAKCgojIyMgRXJyb3IgZGUgdGVzdAoKYGBge3J9CmVycm9yZXNfdGVzdCA8LSBiaW5kX3Jvd3MoCiAgICAgICAgICAgICAgICAgIGVycm9yX3Rlc3RfZ2xtLAogICAgICAgICAgICAgICAgICBlcnJvcl90ZXN0X3JmLAogICAgICAgICAgICAgICAgICBlcnJvcl90ZXN0X3N2bSwKICAgICAgICAgICAgICAgICAgZXJyb3JfdGVzdF9tYXJzCiAgICAgICAgICAgICAgICApCgplcnJvcmVzX3Rlc3QgJT4lIHNlbGVjdChtb2RlbG8sIC5tZXRyaWMsIC5lc3RpbWF0ZSkKYGBgCgpgYGB7cn0KZ2dwbG90KGRhdGEgPSBlcnJvcmVzX3Rlc3QpICsKZ2VvbV9jb2woYWVzKHggPSBtb2RlbG8sIHkgPSAuZXN0aW1hdGUsIGZpbGw9IG1vZGVsbyksIGNvbG9yID0gImdyYXkiKSArCmNvb3JkX2ZsaXAoKSArCmxhYnModGl0bGUgPSAiRXJyb3IgZGUgdGVzdCIpICsKdGhlbWVfYncoKQpgYGAKCgojIFJlZmVyZW5jaWFzCgoKKiBLdWhuIGV0IGFsLiwgKDIwMjApLiBUaWR5bW9kZWxzOiBhIGNvbGxlY3Rpb24gb2YgcGFja2FnZXMgZm9yIG1vZGVsaW5nIGFuZCBtYWNoaW5lIGxlYXJuaW5nIHVzaW5nIHRpZHl2ZXJzZSBwcmluY2lwbGVzLiBodHRwczovL3d3dy50aWR5bW9kZWxzLm9yZwoKKiBHLiBKYW1lcywgRC4gV2l0dGVuLCBULiBIYXN0aWUsIFIuIFRpYnNoaXJhbmkuIEFuIEludHJvZHVjdGlvbiB0byBTdGF0aXN0aWNhbCBMZWFybmluZy4gTUlUIFByZXNzLCAyMDEwLgoKKiBMaW5lYXIgTW9kZWxzIHdpdGggUiBCeSBKdWxpYW4gSi4gRmFyYXdheQoKKiAgW01hY2hpbmUgTGVhcm5pbmcgY29uIFIgeSB0aWR5bW9kZWxzXShodHRwczovL3d3dy5jaWVuY2lhZGVkYXRvcy5uZXQvZG9jdW1lbnRvcy81OV9tYWNoaW5lX2xlYXJuaW5nX2Nvbl9yX3lfdGlkeW1vZGVscyNJbnRyb2R1Y2NpJUMzJUIzbiksIEpvYXF1w61uIEFtYXQgUm9kcmlnby4=